I am using Redux to subscribe to a store and update a component.
This is a simplified example without Redux. It uses a mock-up store to subscribe and dispatch to.
Please, follow the steps below the snippet to reproduce the problem.
Edit: Please skip to the second demo snippet under Update for a more concise and closer to real-life scenario. The question is not about Redux. It's about React's setState function identity causing re-render in certain circumstances even though the state has not changed.
Edit 2: Added even more concise demo under "Update 2".
const {useState, useEffect} = React;
let counter = 0;
const createStore = () => {
const listeners = [];
const subscribe = (fn) => {
listeners.push(fn);
return () => {
listeners.splice(listeners.indexOf(fn), 1);
};
}
const dispatch = () => {
listeners.forEach(fn => fn());
};
return {dispatch, subscribe};
};
const store = createStore();
function Test() {
const [yes, setYes] = useState('yes');
useEffect(() => {
return store.subscribe(() => {
setYes('yes');
});
}, []);
console.log(`Rendered ${++counter}`);
return (
<div>
<h1>{yes}</h1>
<button onClick={() => {
setYes(yes === 'yes' ? 'no' : 'yes');
}}>Toggle</button>
<button onClick={() => {
store.dispatch();
}}>Set to Yes</button>
</div>
);
}
ReactDOM.render(<Test />, document.getElementById('root'));
<div id="root"></div>
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
yes
is already "yes", state is unchanged, hence the component is not re-rendered.yes
is set to "no". State has changed, so the component is re-rendered.yes
is set to "yes". State has changed again, so the component is re-rendered.On step 4 the component should not be re-rendered since state is unchanged.
As the React docs state, useEffect
is
suitable for the many common side effects, like setting up subscriptions and event handlers...
One such use case could be listening to a browser event such as online
and offline
.
In this example we call the function inside useEffect
once when the component first renders, by passing it an empty array []
. The function sets up event listeners for online state changes.
Suppose, in the app's interface we also have a button to manually toggle online state.
Please, follow the steps below the snippet to reproduce the problem.
const {useState, useEffect} = React;
let counter = 0;
function Test() {
const [online, setOnline] = useState(true);
useEffect(() => {
const onOnline = () => {
setOnline(true);
};
const onOffline = () => {
setOnline(false);
};
window.addEventListener('online', onOnline);
window.addEventListener('offline', onOffline);
return () => {
window.removeEventListener('online', onOnline);
window.removeEventListener('offline', onOffline);
}
}, []);
console.log(`Rendered ${++counter}`);
return (
<div>
<h1>{online ? 'Online' : 'Offline'}</h1>
<button onClick={() => {
setOnline(!online);
}}>Toggle</button>
</div>
);
}
ReactDOM.render(<Test />, document.getElementById('root'));
<div id="root"></div>
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
online
is set to false
. State has changed, so the component is re-rendered.online
was already false
, thus state has not changed, but the component is still re-rendered.On step 3 the component should not be re-rendered since state is unchanged.
const {useState, useEffect} = React;
let counterRenderComplete = 0;
let counterRenderStart = 0;
function Test() {
const [yes, setYes] = useState('yes');
console.log(`Component function called ${++counterRenderComplete}`);
useEffect(() => console.log(`Render completed ${++counterRenderStart}`));
return (
<div>
<h1>{yes ? 'yes' : 'no'}</h1>
<button onClick={() => {
setYes(!yes);
}}>Toggle</button>
<button onClick={() => {
setYes('yes');
}}>Set to Yes</button>
</div>
);
}
ReactDOM.render(<Test />, document.getElementById('root'));
<div id="root"></div>
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
yes
is already true
, state is unchanged, hence the component is not re-rendered.yes
is set to false
. State has changed, so the component is re-rendered.yes
is set to true
. State has changed again, so the component is re-rendered.Why is the component still re-rendered? Am I doing something wrong, or is this a React bug?
The answer is that React uses a set of heuristics to determine whether it can avoid calling the rendering function again. These heuristics may change between versions and aren't guaranteed to always bail out when the state is the same. The only guarantee React provides is that it won't re-render child components if the state was the same.
Your rendering functions should be pure. Therefore, it shouldn't matter how many times they run. If you're calculating something expensive in your render function and are concerned about calling it more than necessary, you can wrap that calculation in useMemo
.
Generally speaking, there's no use in "counting renders" in React. When exactly React calls your function is up to React itself, and the exact timing will keep changing between versions. It's not part of the contract.
It seems like this is an expected behavior.
From the React docs:
Bailing out of a state update
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. (React uses the
Object.is
comparison algorithm.)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. If you’re doing expensive calculations while rendering, you can optimize them with
useMemo
.
So, React does re-render the component on step 4 of the first demo and step 3 of the second one. Hence it executes all the code inside the function and it calls React.createElement()
for each child of the component.
However it does not render any descendant of the component and does not fire the effects.
This is only true for function components. For a pure class component, the render
method never gets called if the state has not changed.
There's nothing we can do to avoid the re-run. Memoizing the function with memo()
will not help either, since it only checks for props changes, not the state. So we just have to take this situation into account.
This doesn't answer the question why and when React runs the function but bails out, and when it doesn't run the function at all. If you know the reason, please add your answer.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With