I need to add some event handlers that interact with an object outside of React (think Google Maps as an example). Inside this handler function, I want to access some state that I can send through to this external object.
If I pass the state as a dependency to the effect, it works (I can correctly access the state) but the add/remove handler is added every time the state changes.
If I don't pass the state as the dependency, the add/remove handler is added the appropriate amount of times (essentially once), but the state is never updated (or more accurately, the handler can't pull the latest state).
Perhaps best explained with a Codepen: https://codepen.io/cjke/pen/dyMbMYr?editors=0010
const App = () => {
const [n, setN] = React.useState(0);
React.useEffect(() => {
const os = document.getElementById('outside-react')
const handleMouseOver = () => {
// I know innerHTML isn't "react" - this is an example of interacting with an element outside of React
os.innerHTML = `N=${n}`
}
console.log('Add handler')
os.addEventListener('mouseover', handleMouseOver)
return () => {
console.log('Remove handler')
os.removeEventListener('mouseover', handleMouseOver)
}
}, []) // <-- I can change this to [n] and `n` can be accessed, but add/remove keeps getting invoked
return (
<div>
<button onClick={() => setN(n + 1)}>+</button>
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
If the dep list for the effect is [n]
the state is updated, but add/remove handler is added/removed for every state change. If the dep list for the effect is []
the add/remove handler works perfectly but the state is always 0 (the initial state).
I want a mixture of both. Access the state, but only the useEffect once (as if the dependency was []
).
I know how I can solve it with lifecycle methods, but not sure how it can work with Hooks.
If the above were a class component, it would look like:
class App extends React.Component {
constructor(props) {
super(props)
this.state = { n: 0 };
}
handleMouseOver = () => {
const os = document.getElementById("outside-react");
os.innerHTML = `N=${this.state.n}`;
};
componentDidMount() {
console.log("Add handler");
const os = document.getElementById("outside-react");
os.addEventListener("mouseover", this.handleMouseOver);
}
componentWillUnmount() {
console.log("Remove handler");
const os = document.getElementById("outside-react");
os.removeEventListener("mouseover", handleMouseOver);
}
render() {
const { n } = this.state;
return (
<div>
<strong>Info:</strong> Click button to update N in state, then hover the
orange box. Open the console to see how frequently the handler is
added/removed
<br />
<button onClick={() => this.setState({ n: n + 1 })}>+</button>
<br />
state inside react: {n}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
Noting how the add/remove handler is only added once (obviously ignoring the fact that the App component isn't unmounted), despite the state change.
I'm looking for a way to replicate that with hooks
With the empty dependency array the useEffect will be run only once. And it will access the state from that one run. So it will have a reference from the logAbandonListing function from this moment. This function will access the state from this moment also.
It's ok to use setState in useEffect you just need to have attention as described already to not create a loop. The reason why this happen in this example it's because both useEffects run in the same react cycle when you change both prop.
Use the useEffect hook to listen for state changes in React. You can add the state variables you want to track to the hook's dependencies array and the logic in your useEffect hook will run every time the state variables change.
▶ 1. Can I set state inside a useEffect hook? In principle, you can set state freely where you need it - including inside useEffect and even during rendering. Just make sure to avoid infinite loops by settting Hook deps properly and/or state conditionally. ▶ 2. Lets say I have some state that is dependent on some other state.
Generally speaking, using setState inside useEffect will create an infinite loop that most likely you don't want to cause. There are a couple of exceptions to that rule which I will get into later.
You can also pass variables on which useEffect depends to re-run the logic passed into the useEffect .The empty array will run the effect hook only once. We can also use the useEffect method as a cleanup function once the component will destroy.The useEffect can return a function to clean up the effect as like componentWillUnmount () method:
The effect is executed every time the prop changes. Let’s extend the example a bit to demonstrate more pivotal concepts in conjunction with prop changes. I added log statements to indicate all component renderings, as well as the invocation of our useEffect statement.
You can use mutable refs to decouple reading current state from effect dependencies:
const [n, setN] = useState(0);
const nRef = useRef(n); // define mutable ref
useEffect(() => { nRef.current = n }) // nRef is updated after each render
useEffect(() => {
const handleMouseOver = () => {
os.innerHTML = `N=${nRef.current}` // n always has latest state here
}
os.addEventListener('mouseover', handleMouseOver)
return () => { os.removeEventListener('mouseover', handleMouseOver) }
}, []) // no need to set dependencies
const App = () => {
const [n, setN] = React.useState(0);
const nRef = React.useRef(n); // define mutable ref
React.useEffect(() => { nRef.current = n }) // nRef.current is updated after each render
React.useEffect(() => {
const os = document.getElementById('outside-react')
const handleMouseOver = () => {
os.innerHTML = `N=${nRef.current}` // n always has latest state here
}
os.addEventListener('mouseover', handleMouseOver)
return () => { os.removeEventListener('mouseover', handleMouseOver) }
}, []) // no need to set dependencies
return (
<div>
<button onClick={() => setN(prev => prev + 1)}>+</button>
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>
<div id="outside-react">div</div>
<p>Update counter with + button, then mouseover the div to see recent counter state.</p>
The event listener will be added/removed only once on mounting/unmounting. Current state n
can be read inside useEffect
without setting it as dependency ([]
deps), so there is no re-triggering on changes.
You can think of useRef
as mutable instance variables for function components and Hooks. The equivalent in class components would be the this
context - that is why this.state.n
in handleMouseOver
of the class component example always returns latest state and works.
There is a great example by Dan Abramov showcasing above pattern with setInterval
. The blog post also illustrates potential problems with useCallback
and when an event listener is readded/removed with every state change.
Other useful examples are (global) event handlers like os.addEventListener
or integration with external libraries/frameworks at the edges of React.
Note: React docs recommend to use this pattern sparingly. From my point of view, it is a viable alternative in situations, where you just need "the latest state" - independent of React render cycle updates. By using mutable variables, we break out of the function closure scope with potentially stale closure values.
Writing state independently from dependencies has further alternatives - you can take a look at How to register event with useEffect hooks? for more infos.
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