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>
What is happening
- ✅ Click "Set to Yes". Since the value of
yes
is already "yes", state is unchanged, hence the component is not re-rendered. - ✅ Click "Toggle".
yes
is set to "no". State has changed, so the component is re-rendered. - ✅ Click "Set to Yes".
yes
is set to "yes". State has changed again, so the component is re-rendered. - ⛔ Click "Set to Yes" again. State has not changed, but the component is still re-rendered.
- ✅ Subsequent clicks on "Set to Yes" do not cause re-rendering as expected.
What is expected to happen
On step 4 the component should not be re-rendered since state is unchanged.
Update
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>
What is happening
- ✅ The component is first rendered on the screen, and the message is logged in the console.
- ✅ Click "Toggle".
online
is set tofalse
. State has changed, so the component is re-rendered. - ⛔ Open Dev tools and in the Network panel switch to "offline".
online
was alreadyfalse
, thus state has not changed, but the component is still re-rendered.
What is expected to happen
On step 3 the component should not be re-rendered since state is unchanged.
Update 2
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>
What is happening
- ✅ Click "Set to Yes". Since the value of
yes
is alreadytrue
, state is unchanged, hence the component is not re-rendered. - ✅ Click "Toggle".
yes
is set tofalse
. State has changed, so the component is re-rendered. - ✅ Click "Set to Yes".
yes
is set totrue
. State has changed again, so the component is re-rendered. - ⛔ Click "Set to Yes" again. State has not changed, despite that the component starts the rendering process by calling the function. Nevertheless, React bails out of rendering somewhere in the middle of the process, and effects are not called.
- ✅ Subsequent clicks on "Set to Yes" do not cause re-rendering (function calls) as expected.
Question
Why is the component still re-rendered? Am I doing something wrong, or is this a React bug?