I wrote a simple React to test how React render
import ReactDOM from 'react-dom';
import React, { useState, useEffect } from 'react';
const App = (props) => {
// State
const [trackIndex, setTrackIndex] = useState(1);
const [trackProgress, setTrackProgress] = useState(0);
const clickHandler = () =>{
setTrackIndex((trackIndex)=>trackIndex+1)
}
useEffect(() => {
console.log("effect; trackIndex=",trackIndex)
console.log("effect; trackProgress=",trackProgress)
if(trackIndex<5){
console.log("set trackProgress")
setTrackProgress(trackIndex)
}
});
console.log("render")
return (
<div>
<p>{trackIndex}</p>
<p>{trackProgress}</p>
<button onClick={clickHandler}>Click me</button>
</div>
);
};
ReactDOM.render(
<App />,
document.getElementById('root')
);
The following is the console output:
render
effect; trackIndex= 1
effect; trackProgress= 0
set trackProgress
render
effect; trackIndex= 1
effect; trackProgress= 1
set trackProgress
render
It seems that React renders three times before I click the button. The last rendering really confuses me. Could anyone explain to me why this render occurs and why no effect runs after this rendering? Thank you in advance
It seems that your useEffect is setting the state with the setTrackProgress on the initial render of the component. This is because trackIndex starts at 1 which is lower than 5. (seen by the if(trackIndex<5)) Notice that you haven't provided array dependency to useEffect as the second parameter. According to react-documentation this means the effect will only occur once after the first initial render of the component.
Also, I would suggest adding useCallback to your clickHandler in order to prevent re-defining the function for every render. (According to react-documentation)
You're practically making an infinite loop, but React saves you. The first render log is the initial render, then the useEffect is executed and your component re-renders, leading to the 2nd render log and the first effect; ... log. Now a 2nd useEffect is executed. However, the value of trackIndex hasn't changed, so your if statement will evaluate true and updates trackProgress with the same state/value(1). This leads to another re-render and the third render log. Now, you would think that a 3rd useEffect would be executed, but React is smart enough to know when state hasn't changed and thus doesn't execute the useEffect.
Add dependencies to your useEffect as stated above. That'll solve your problem.
useEffect(
...
, [trackIndex])
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 have a simple React component and I set the same value each time that I click on the button:
import React, { useState } from 'react';
import './style.css';
let data = { title: 'ABC' };
export default function App() {
const [foo, setFoo] = useState();
console.log('Rendered');
return (
<div>
<button onClick={() => setFoo(data)}>Set Data</button>
<h1>Data: {JSON.stringify(foo)}</h1>
</div>
);
}
But renders are a little bit strange because at the second button click React renders the component but I set the same value.
Why React re-render the component although I set the same value?
Demo Here
The question is about why the component renders although the new state equals the previous state (shallow comparison)
// On second button click
const prevState = data
// State trigger
setFoo(data)
// Same state, it doesn't triggers a render
data === prevState
So, the component didn't trigger render due to state change.
But it happened due to another reason, as mentioned in docs "between the lines" under Hooks API Bailing out of a state update section:
Note that React may still need to render that specific component again before bailing out.
Unlike in class component, for function components, after setting the same state like in our case, sometimes, React will need another render to validate its equality. Its unfortunate edge case.
But it should not consider you as "performance issue" since it does not effect the React.Node tree, it won't continue in the reconciliation process if the state didn't change. It even won't make unnecessary hooks calls.
Another Simplified Example
Same goes here, there is another render for bail out, another log of "A".
Although there is a "bail out" render, notice that the useEffect does not run.
import React, { useEffect } from "react";
import ReactDOM from "react-dom";
const App = () => {
const [state, setState] = React.useState(0);
useEffect(() => {
console.log("B");
});
console.log("A");
return (
<>
<h1>{state}</h1>
<button onClick={() => setState(42)}>Click</button>
</>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
If you wondering on logs order ("Why 'A' logged before 'B'?"), try deep diving another question: React useEffect in depth / use of useEffect?
In functional component, a component isn't re-rendered if it's the same value, i.e. a value that passes === comparison:
const [state, setState] = useState({});
...
setState(state => state); // no re-render
Otherwise a component is re-rendered:
setState(state => ({...state})); // re-render
As commented already, it may be "rerendered" by react, but the DOM will likely not change.
Checkout https://github.com/facebook/react/issues/17474
This problem occurs only if the state value was actually changed due to the previous update.
In the following example, when the button is clicked for the first time, "setState" is called with a new value (of 12), and a component update occurs, which is understandable.
When I click the same button for the second time, setting the state to the same value of 12 it causes the component to re-run (re-render), and why exactly that happens is my main question.
Any subsequent setStates to the same value of 12 will not trigger a component update, which is again, understandable. 12 === 12 so no update is needed.
So, why is the update happening on the second click of the button?
export default function App() {
const [state, setState] = useState(0);
console.log("Component updated");
return (
<div className="App">
<h1>Hello CodeSandbox {state}</h1>
<button onClick={() => setState(12)}>Button</button>
</div>
);
}
Codesandbox example
The main question is, why logging in function component body causes 3 logs of "Component updated"?
The answer is hiding somewhere in React docs:
if you update a State Hook to the same value as the current state, React will bail out without rendering the children or firing effects.
Nothing new, but then:
Note that React may still need to render that specific component again before bailing out. That shouldn’t be a concern because React won’t unnecessarily go “deeper” into the tree.
But notice useEffect API definition:
will run after the render is committed to the screen.
If you log the change in useEffect you notice only two "B" logs as expected, which is exactly the example for bail out behavior mentioned:
const App = () => {
const [state, setState] = React.useState(0);
useEffect(() => {
console.log("B");
});
console.log("A");
return (
<>
<h1>{state}</h1>
<button onClick={() => setState(42)}>Click</button>
</>
);
};
There will be an additional "Bail out" call for App component (extra "A" log), but React won't go "deeper" and won't change the existing JSX or state (no additional "B" will be logged).
Adding to the generally correct accepted answer, there is what i've found diving deeper in that problem:
There is actually more complex mechanics in that.
Actually, any setState call is applying reducer function under the hood, and that reducer function runs on next useState call, not before component function execution.
So normally there is no way of knowing if new state will be the same or not without executing executing that reducer (on useState call).
On the other hand however, when such reducer was once executed and state was not changed, component's return is ignored (render skipped) and next call of that reducer will be executed before component's function and NOT when useState called. That is also true for the very first setState of the component's life.
I've made a demo of that in codesandbox
Can't find documentation about this anywhere. Will this cause the useEffect to EVER run again? I don't want it to fetch twice, that would cause some issues in my code.
import React, { useEffect } from 'react'
import { useHistory } from 'react-router-dom'
const myComponent = () => {
const { push } = useHistory();
useEffect( () => {
console.log(" THIS SHOULD RUN ONLY ONCE ");
fetch(/*something*/)
.then( () => push('/login') );
}, [push]);
return <p> Hello, World! </p>
}
From testing, it doesn't ever run twice. Is there a case that it would?
For the sake of the question, assume that the component's parent is rerendering often, and so this component is as well. The push function doesn't seem to change between renders - will it ever?
Ciao, the way you write useEffect is absolutely right. And useEffect will be not triggered an infinite number of time. As you said, push function doesn't change between renders.
So you correctly added push on useEffect deps list in order to be called after fetch request. I can't see any error in your code.
I'm having a really hard time to figure out what's happening when there is nothing being used to trigger re-render the component.
Events.js Component renders twice when I remove the useState() from the Event.js it renders once, but I need to keep it. when I use useEffect() inside Event components, renders fourth time.
I just kept the dummy data to give you to fill the emptiness and tried to remove React.memo, nothing happens. the problem is with the Event.js component I believe. I'm also using the Context API, but forth time rendering is too much.
useEffect inside App.js is getting some value from the localStorage, I can't access that direct 'cause the value is undefined by default
sandbox code here: https://codesandbox.io/s/event-manager-reactjs-nbz8z?file=/src/Pages/Events/Events.js
The Events.js file is located on /Pages/Events/Events.js
example code is below
Event.js ( child component )
function Events() {
// Sate Managing
const [allEvents, setAllEvents] = React.useState(null);
console.log('Rendering EventsJs', allEvents);
React.useEffect(() => {
setAllEvents(['apple', 'banana']);
}, []);
return (
<div className="events">
{ console.log('Event Rendered.js =>') }
</div>
)
}
export default React.memo(Events, (prevProps, nextProps) => {
return true;
} );
App.js ( parent component )
import { BrowserRouter, Route, Redirect } from 'react-router-dom';
function App() {
const [userId, setUserId] = React.useState(null);
React.useEffect(() => {
setUserId(1);
}, []);
// Login
return (
<BrowserRouter>
<Navigation />
<Route path='/events' component={Events} />
{console.log('App Rendered')}
</BrowserRouter>
);
}
export default App;
Error:
Your app is working fine. It is rendering as it should. As we know:
A React component re-renders whenever its props or state change.
And react component lifecycle order is:
Initial props/state --> render --> DOM update --> mounted
props/state changed --> render --> DOM update --> updated ... so on
In the example below, it is rendering 2 times and that's correct:
First one (first console.log) is due to initial render with state as []
Second one (second console.log) is due to state change (caused by useEffect) to ['apple', 'banana']
function Events() {
const [allEvents, setAllEvents] = React.useState([]);
console.log('Event Rendered', allEvents);
useEffect(() => {
setAllEvents(['apple', 'banana']);
}, []);
return <>Events</>;
}
About using React.memo:
React.memo only checks for props changes. If your function component wrapped in React.memo has a useState or useContext Hook in its implementation, it will still rerender when state or context change.
You can not skip re-render using React.memo due to change in state. You can only optimize to skip re-rendering caused by change in props.
But in the example above, you don't have props passed from the parent component, the only props passed to Events are those passed by react-router i.e. route props. So, there is no need to use React.memo.
Here is sandbox, check the console.logs. You will see only 3 logs: "App render", "Event render with initial state", "Event render with new state".
EDIT:
If we remove StrictMode from index.html, and add below console.logs in components:
App.js --> console.log('App rendered')
Evenets.js --> console.log('Event rendered', allEvents, isLoading) // (allEvents and isLoading are state variables here)
And go to http://localhost:3000, we see 1 log:
App Rendered
Now click on "Events", we see 3 logs:
1: Event Rendered, [], true
2: Event Rendered, [{}, ... 54 items], true
3: Event Rendered, [{}, ... 54 items], false
which is correct behavior (refer lifecycles order written above):
1st log: render with initial state ([], true)
2nd log: render with new allEvents (54 items) and old isLoading (true)
3rd log: render with old allEvents (54 items) and new isLoading (false)
Below are the right questions to ask now:
Question1:
Why 2nd and 3rd render (log) are separate, should not they be batched (merged) and applied together as they are written in the same function?
fetch('url').then(() => {
// ... code here
setAllEvents([...events])
setLoading(false)
})
Answer:
No, they will not be batched in above code. As explained by Dan Abramov:
This is implementation detail and may change in future versions.
In current release, they will be batched together if you are inside a React event handler. React batches all setStates done during a React event handler, and applies them just before exiting its own browser event handler.
With current version, several setStates outside of event handlers (e.g. in network responses) will not be batched. So you would get two re-renders in that case.
There exists a temporary API to force batching. If you write ReactDOM.unstable_batchedUpdates(() => { this.fn1(); }); then both calls will be batched. But we expect to remove this API in the future and instead batch everything by default.
So, you can write (inside fetch's then), if you want, it will save 1 render:
ReactDOM.unstable_batchedUpdates(() => {
setAllEvents([...events])
setLoading(false)
})
Question2:
What's React event handler in above quote?
Answer: foo in example below. These 2 set states will be batched.
const foo = () => {
setAllEvents([
{ _id: '5ede5af03915bc469a9d598e', title: 'jfklsd', },
])
setLoading(false)
}
<button onClick={foo}>CLICK</button>
Question3:
Does it update HTML DOM as many times as it renders (prints console.log)?
Answer: No. React compares calculated virtual DOMs before updating real DOM, so only those changes are applied to real DOM which are required to update the UI.
Question4:
Why was rendering doubled when we use StrictMode?
Answer: Yes, StrictMode will intentionally double invoke "render" and some other lifecycle methods to detect side-effects. Strict mode checks are run in development mode only; they do not impact the production build.
Well actually this is caused by your usage of React.memo, its second parameter is called areEqual, and you pass in () => false, so you are basically telling React that the props are always changing. Therefore whenever App rerenders, Events rerenders too.
You should let React.memo check for prop changes. By passing () => false you are actually telling that its props always change (they are never equal).
export default React.memo(Events);
Here's a working example.