This question already has answers here:
React useState cause double rendering
(3 answers)
What is StrictMode in React?
(4 answers)
Closed 1 year ago.
So after working with react in the past year, i managed to understand its powers and caveats, and how to avoid unnecessary renders.
Yesterday i was playing with some code and came across an issue i didnt see before, and got a little confuse.
import React, { useCallback, useState } from "react";
let renders = 0;
const Counter = () => {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount((c) => c + 1);
}, []);
renders = renders + 1;
console.log("Counter Render");
return (
<div>
<button onClick={increment}>increment</button>
<h2>renders: {renders}</h2>
<h4>Count: {count}</h4>
</div>
);
};
export default React.memo(Counter);
in the above code i added a simple counter, on every click i am setting a new state, which cause a re-render, showing "Count: 1" on the screen and one "Counter Render" log in dev tools - just like i expected.
The weird part comes from the renders variable, which i initiate with the number 0 ( outside the component, offcourse ) and increment on every render of the component.
i would have expect the value here will also be 1 but that is not the case, every time i click the button the value of "renders" grows by 2, even though the "Counter Render" log show once each time.
Here is a the Sandbox example
can anyone explain what am i missing here ?
You're running in React's Strict Mode (since your app is wrapped in <StrictMode>).
Strict Mode may render components multiple times to make sure you're not doing naughty things such as relying on render counts – which you now are. ;-)
Related
As the function body of React function component is like the render function of the React class component, the React function component should be executed repeatedly whenever it's rendered.
So I wrote below code to verify this behavior.
import { useEffect, useState } from "react";
import "./styles.css";
let i = 0;
export default function App() {
console.log("App:", ++i);
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => setCount(count + 1), 2000);
}, []);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Count:{count}</h2>
</div>
);
}
I expect the console output as below:
App: 1
App: 2
But the actual result is as below:
App: 1
App: 3
App: 5
Please check:Example code in codesandbox
My understanding is that, i should be 1 when App component is executed for the first time.
After 2 seconds, setCount(0+1) update count from 0 to 1, React triggers a re-rendering which run App function for the second time, and should print "App: 2" in console. Every 2 seconds after that, setCount(0+1) will be executed repeatedly without triggering any count update as count is always 1 after that.
But the actual result is totally different from my expectation.
Very appreciate if anyone could help correcting my misunderstanding.
How many times your component function is called doesn't matter and shouldn't matter for your app.
React is absolutely free to call your component function a thousand times per update if it feels like it (and indeed, in strict mode it does so twice); that's why you're supposed to make them idempotent and to use React.useMemo if you have heavy computations.
However, your component is broken in that the effect doesn't capture count, so the count may not correctly increase. (The rules of hooks ESlint rules will let you know.) You'd want the function form of setState to derive the new state from the old one:
setCount(count => count + 1);
I am trying to learn React by building a web application. Since I want to learn it step by step, for now I don't use Redux, I use only the React state and I have an issue.
This is my components architecture:
App.js
|
_________|_________
| |
Main.js Side.js
| |
Game.js Moves.js
As you can see, I have the main file called App.js, in the left side we have the Main.js which is the central part of the application which contains Game.js where actually my game is happening. On the right side we have Side.js which is the sidebar where I want to display the moves each player does in the game. They will be displayed in Moves.js.
To be more clear think at the chess game. In the left part you actually play the game and in the right part your moves will be listed.
Now I will show you my code and explain what the problem is.
// App.js
const App = React.memo(props => {
let [moveList, setMovesList] = useState([]);
return (
<React.Fragment>
<div className="col-8">
<Main setMovesList={setMovesList} />
</div>
<div className="col-4">
<Side moveList={moveList} />
</div>
</React.Fragment>
);
});
// Main.js
const Main = React.memo(props => {
return (
<React.Fragment>
<Game setMovesList={props.setMovesList} />
</React.Fragment>
);
});
// Game.js
const Game= React.memo(props => {
useEffect(() => {
function executeMove(e) {
props.setMovesList(e.target);
}
document.getElementById('board').addEventListener('click', executeMove, false);
return () => {
document.getElementById('board').removeEventListener('click', executeMove, false);
};
})
return (
// render the game board
);
});
// Side.js
const Side= React.memo(props => {
return (
<React.Fragment>
<Moves moveList={props.moveList} />
</React.Fragment>
);
});
// Moves.js
const Moves= React.memo(props => {
let [listItems, setListItems] = useState([]);
useEffect(() => {
let items = [];
for (let i = 0; i < props.moveList.length; i++) {
items.push(<div key={i+1}><div>{i+1}</div><div>{props.moveList[i]}</div></div>)
}
setListItems(items);
return () => {
console.log('why this is being triggered on each move?')
};
}, [props.moveList]);
return (
<React.Fragment>
{listItems}
</React.Fragment>
);
});
As you can see on my code, I have defined the state in App.js. On the left side I pass the function which updates the state based on the moves the player does. On the right side I pass the state in order to update the view.
My problem is that on each click event inside Game.js the component Moves.js unmounts and that console.log is being triggered and I wasn't expected it to behave like that. I was expecting that it will unmount only when I change a view to another.
Any idea why this is happening ? Feel free to ask me anything if what I wrote does not make sense.
Thanks for explaining your question so well - it was really easy to understand.
Now, the thing is, your component isn't actually unmounting. You've passed props.movesList as a dependency for the usEffect. Now the first time your useEffect is triggered, it will set up the return statement. The next time the useEffect gets triggered due to a change in props.movesList, the return statement will get executed.
If you intend to execute something on unmount of a component - shift it to another useEffect with an empty dependency array.
answering your question
The answer to your question
"why this is being triggered on each move"
would be:
"because useEffect wants to update the component with the changed state"
But I would be inclined to say:
"you should not ask this question, you should not care"
understanding useEffect
You should understand useEffect as something that makes sure the state is up to date, not as a kind of lifecycle hook.
Imagine for a moment that useEffect gets called all the time, over and over again, just to make sure everything is up to date. This is not true, but this mental model might help to understand.
You don't care if and when useEffect gets called, you only care about if the state is correct.
The function returned from useEffect should clean up its own stuff (e.g. the eventlisteners), again, making sure everything is clean and up to date, but it is not a onUnmount handler.
understanding React hooks
You should get used to the idea that every functional component and every hook is called over and over again. React decides if it might not be necessary.
If you really have performance problems, you might use e.g. React.memo and useCallback, but even then, do not rely on that anything is not called anymore.
React might call your function anyway, if it thinks it is necessary. Use React.memo only as kind of a hint to react to do some optimization here.
more React tips
work on state
display the state
E.g. do not create a list of <div>, as you did, instead, create a list of e.g. objects, and render that list inside the view. You might even create an own MovesView component, only displaying the list. That might be a bit too much separation in your example, but you should get used to the idea, also I assume your real component will be much bigger at the end.
Don’t be afraid to split components into smaller components.
It seems the problem is occurred by Game element.
It triggers addEventListener on every render.
Why not use onClick event handler
/* remove this part
useEffect(() => {
function executeMove(e) {
props.setMovesList(e.target);
}
document.getElementById('board').addEventListener('click', executeMove, false);
})
*/
const executeMove = (e) => {
props.setMovesList(e.target);
}
return (
<div id="board" onClick={executeMove}>
...
</div>
)
If you want to use addEventListener, it should be added when the component mounted. Pass empty array([]) to useEffect as second parameter.
useEffect(() => {
function executeMove(e) {
props.setMovesList(e.target);
}
document.getElementById('board').addEventListener('click', executeMove, false);
}, [])
This question already has an answer here:
Why does useState cause the component to render twice on each update?
(1 answer)
Closed 2 years ago.
const App = () => {
const [ counter, setCounter ] = useState(0)
console.log(counter)
return (
<>
<div>You clicked {counter} times.</div>
<button onClick={ () => setCounter(counter+1) }>Click me!</button>
</>
)
}
Here's my react component. My question is when I run this, I see 0 two times in the console. Then when I click on the button, I see 1 two times in the console. Can anyone explain why does that happen? I was expecting 0, 1, 2 to be printed only once in the console whenever I click on the button.
Please forgive if this question has already been answered or my title of the question is not related with what I'm asking as this is my first question here.
It is because of React.StrictMode
If you go to your index.js , you will find that your App component is wrapped with <React.StrictMode>. If you strip off the StrictMode you will notice that your App component will render only once.
Refer the below doc to understand StrictMode
Strict mode can’t automatically detect side effects for you, but it
can help you spot them by making them a little more deterministic.
This is done by intentionally double-invoking the following functions:
Class component constructor, render, and shouldComponentUpdate methods
Class component static getDerivedStateFromProps method Function
component bodies State updater functions (the first argument to
setState) Functions passed to useState, useMemo, or useReducer
Please refer: https://reactjs.org/docs/strict-mode.html
return (<React.StrictMode><App /><React.StrictMode>)
This would solve your problem.
I have got 2 pieces of code one is using independent state in child and the other one with state being send out as props like this in my " sent as props " version.
function App() {
const [isChanged, setIsChanged] = React.useState(false);
const [i, setI] = React.useState(0);
React.useEffect(() => {
console.log("USEEFFECT");
setTimeout(() => {
setIsChanged(true);
}, 2000);
}, []);
return (
<div className="App zx">
{isChanged && <h1>Hello CodeSandbox with outter states</h1>}
<Change set={setI} i={i} />
</div>
);
}
the second one with the exception of having states inside <Change /> as such :
function Change() {
const [i, setI] = React.useState(0);
let rnd = 9;
if (i !== rnd) {
setI(i + 1);
}
console.log(i);
The version in which state is managed inside the child, the component runs twice and I get the log two times in a row but in the one with passed down state as props I get the component running once as desired.
Why is this happening and which one is the correct practice ?
When can I safely use state in a child component without worrying about re-renders ?
Is there a way to avoid the re-render in the first version so I get
the same results despite using state in the child component ? If so please provide me with a sample.
To reproduce,
Props version : https://codesandbox.io/s/heuristic-driscoll-9m1pl
state in child version :
https://codesandbox.io/s/confident-shockley-btjg6
Usually you put the state in the component where you want to use it. For example, if just one child uses the state, put it right in the child component. But let's say you want to use the same state in some different child components, then it's better to have it in the parent component. (in this case, it would be better to use useContext() hook).
Honestly, I don't understand what you want to accomplish with your code but in general, rendering and re-rendering happens when you update your state. The reason it re-render agin and you see 0 to 9 and again 0 to 9 is that your default state is 0 and each time it get's re-rendered, the state changes to 0. (I assume)
Hope this answers some of your questions.
I'm getting started with a new create-react-app application using TypeScript, hooks, and mobx-react-lite. Despite having used MobX extensively in a React Native app in the past, I've run into an issue that doesn't make any sense to me.
I have a store with two observables: one number and one boolean. There is an initialize() method that runs some library code, and in the success callback, it sets the number and the boolean to different values (see Line A and Line B below).
The issue: my component ONLY re-renders itself when Line A is present. In that case, after the initialization is complete, the 'ready' text appears, and the button appears. If I delete Line B, the 'ready' text still appears. But if I delete Line A (and keep Line B), the button never renders. I've checked things over a hundred times, everything is imported correctly, I have decorator support turned on. I can't imagine why observing a number can trigger a re-render but observing a boolean cannot. I'm afraid I'm missing something horribly obvious here. Any ideas?
The relevant, simplified code is as follows:
// store/app.store.ts
export class AppStore {
#observable ready = false
#observable x = 5
initialize() {
// Takes a callback
ThirdPartyService.init(() => {
this.ready = true
this.x = 10
})
}
}
// context/stores.ts
const appStore = new AppStore()
const storesContext = React.createContext({
appStore
})
export const useStores = () => React.useContext(storesContext)
// App.tsx
const App = observer(() => {
const { appStore } = useStores()
useEffect(() => {
appStore.initialize()
}, [appStore])
return (
<div>
{ appStore.x === 10 && 'ready' } // <-- Line A
{ appStore.ready && <button>Go</button> } // <-- Line B
</div>
)
}
EDIT: A bit more information. I've added some logging statements to just before the return statement for the App component. I also refactored the button conditional to a const. This may provide more insight:
const button = appStore.ready ? <button>Go</button> : null
console.log('render', appStore.ready)
console.log('button', button)
return (
<div className="App">
<header className="App-header">{button}</header>
</div>
)
When appStore.ready is updated, the component does re-render, but the DOM isn't updated. The console shows 'render' true and shows a representation of the button, as it should, but inspecting the document itself shows no button there. Somehow, though, changing the condition from appStore.ready to appStore.x === 10 does update the DOM.
Turns out I didn't quite give complete information in my question. While I was creating a minimal reproduction, I decided to try dropping the top-level <React.StrictMode> component from index.tsx. Suddenly, everything worked. As it happens, mobx-react-lite#1.5.2, the most up-to-date stable release at the time of my project's creation, does not play nice with Strict Mode. Until it's added to a stable release, the two options are:
Remove strict mode from the React component tree
Use mobx-react-lite#next