I have this callback function in which I set some state. The state seems to mutate just fine. However, I cannot refer to the updated state inside the callback function - it always prints out the initial state.
So my question is why this happens and how should I proceed if i want to check on the updated state inside the callback?
import React, { useState, useEffect } from "react";
import Context from "./ctx";
export default props => {
const [state, setState] = useState({
x: 0,
y: 0
});
useEffect(() => {
window.addEventListener("resize", e => {
setState({
x: e.target.window.visualViewport.width,
y: e.target.window.visualViewport.height
});
console.log(`${JSON.stringify(state)}`); <<---logs the initial state.
});
return () => {
window.removeEventListener("resize");
};
}, []);
console.log(`${JSON.stringify(state)}`); <<---logs updated versions of the state.
return (
<Context.Provider
value={{
...state
}}
>
{props.children}
</Context.Provider>
);
};
The problem you're facing is related to two things:
1. How React works
You've created and exported functional React component, which accepts some props, uses hooks for state management, and renders some content.
Whenever some props or state change in your component (or in it's parent component) react will re-render your component, meaning: it will literally call your function, like yourComponent(props)
.
The point is: the body of your function will be executed every time re-render happens, alongside with calls to useState
and useEffect
.
2. Closures (function + environment in which it was created)
Whenever we create/define some function in JavaScript it is stored in the runtime memory alongside with the environment in which it was created.
In your case, you're defining the function in question (and providing it as a callback to useEffect
at the same time) here:
() => {
window.addEventListener("resize", e => {
setState({
x: e.target.window.visualViewport.width,
y: e.target.window.visualViewport.height
});
console.log(`${JSON.stringify(state)}`); <<---logs the initial state.
});
return () => {
window.removeEventListener("resize");
};
}
and the environment in which it's created is the body of your functional component.
So, whenever React calls your component the new environment is created, and new callback function is defined. But, because you provided empty array as second argument (which represents dependencies array docs) here:
useEffect(() => {
window.addEventListener("resize", e => {
setState({
x: e.target.window.visualViewport.width,
y: e.target.window.visualViewport.height
});
console.log(`${JSON.stringify(state)}`);
});
return () => {
window.removeEventListener("resize");
};
}, []); // <-- HERE
provided function will be executed only once - on component mount/first render, because you said it doesn't depend on any other value, so there's no need to fire that effect again.
Bottom line: You're defining new callback function on every render, but only first one was actually called. When it was defined, value of state was { x: 0, y: 0 }
. That's why you're always getting that value.
Solution
Depending on your needs, you can set dependencies in the second argument, like:
useEffect(() => {
window.addEventListener("resize", e => {
setState({
x: e.target.window.visualViewport.width,
y: e.target.window.visualViewport.height
});
console.log(`${JSON.stringify(state)}`);
});
return () => {
window.removeEventListener("resize");
};
}, [state]); // <-- NOW THE CALLBACK WILL BE EXECUTED EVERY TIME STATE CHANGES
or, you can omit second argument altogether, that way it will be fired on every render:
useEffect(() => {
window.addEventListener("resize", e => {
setState({
x: e.target.window.visualViewport.width,
y: e.target.window.visualViewport.height
});
console.log(`${JSON.stringify(state)}`);
});
return () => {
window.removeEventListener("resize");
};
}); // <-- WITHOUT SECOND ARGUMENT
It may seem expensive, but the official React documentation says it shouldn't have any performance hits with event listeners usage, because addEventListener DOM api is very efficient. But if you notice some performance issues, you can always use the first solution :).
Hope it helps!
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