React: useState hook with callback
In the old “class” oriented React version you could call `setState` and pass as a second argument function which would be called when state of component would be updated.
this.setState(newState, myCallback)
But with new “functional” oriented React where you described components using plain functions you should use `useState` hook to track internal component’s state inside function.
const [state, setState] = useState(null);setState(newState, myCallback)
The `setState` above would throw warning and don’t call `myCallback` because `useState` does not support callbacks and say that you should use `useEffect` for this purpose.
So lets try to fix this in place using `useEffect` hook.
const [state, setState] = useState(null);useEffect(() => {myCallback()}, [state])setState(newState)
`useEffect` accepts two arguments. The first argument is an “effect” function which would be called when component would be rendered on the screen. The second, optional, argument is an array of dependencies. When one of dependency has changed effect function would be called again. In our case we use “state” as a dependency so our effect function would be called two times. The first time when we just render component and the second time when component re-rendered after “setState”. This is looks like a solution which we want but in the “class” version of React
- We don’t call callback on the first render. We call only when we has changed state
- We pass callback in
setState
. This means that callback relates not only to state change but also particular state update. Sometimes we don’t want to trigger callback on every state change but only on particular
To eliminate the first problem we could track “first” render using useRef
. Because first render is an implicit state of component we should make keep this state somewhere in our component we could use useState but because “first” happens only once and don’t require re-render component we would use useRef which is a mutable container which persist between diffirent component’s renders:
const [state, setState] = useState(null);const isFirstRender = useRef(true);useEffect(() => { if (isFirstRender.current) { isFirstRender.current = false; return } myCallback()}, [state])setState(newState)
Looks ugly but it works. But what if we need call several callbacks? We could just add them to “useEffect” function:
const [state, setState] = useState(null);const isFirstRender = useRef(true);useEffect(() => { if (isFirstRender.current) {
…
} myCallback() myCallback2() myCallback3()}, [state])setState(newState)
Ok, what if we need N callbacks, how would looks useEffect?
useEffect(() => { myCallback() myCallback2() … myCallbackN-1() myCallbackN()}, [state])
We can put all our callbacks in a collection and then iterate over it in useEffect
const myCallbacksList = [myCallback, myCallback2,…,myCallbackN];useEffect(() => { myCallbacksList.forEach((callback) => callback())}, [state]);
Because we replace hardcoded functions call expressions to a dynamic iteration and call. We can add our callbacks in this collection dynamically for example when we call setState
setState(newState)myCallbacksList.push(myCallback)
This already start looks like we want but behave not as we expect because “myCollectionList” would be recreated every time when we call our component funciton. What we really want is keep our callbacks collection as a part of component state. Let’s fix it. I’ll use “ref” but you can change implementation on “setState”:
const myCallbacksList = useRef([]);useEffect(() => { myCallbacksList.current.forEach((callback) => callback())}, [state]);
Well, now it start behave as we want but we have a problem, our callbacks no one remove after execution, let’s fix this problem:
const myCallbacksList = useRef([]);useEffect(() => { myCallbacksList.current.forEach((callback) => callback()) myCallbacksList.current = [];}, [state]);
I have fixed it buy reseting list with empty array but you can just pop/shift functions from an array to reduce pressure on garbage collection.
At this point we can write code like this:
setState(newState)myCallbacksList.current.push(myCallback)..setState(newState2)myCallbacksList.current.push(myCallback2)
Let’s wrap this code in meaningful abstraction like setStateWitchCallback
:
setStateWithCallback(newState, myCallback);
To do this we need just create a function in our component which would wrap this duplicaitons:
const [state, setState] = useState(null);const myCallbacksList = useRef([]);const setStateWithCallback= (newState, callback) => { setState(state); if(callback) myCallbackList.current.push(callback)}
In this case we don’t need track first render because when we call render on our component first time our collection of callbacks would be empty. So the final code would look like this:
const [state, setState] = useState(null);const myCallbacksList = useRef([]);const setStateWithCallback= (newState, callback) => { setState(state); if(callback) myCallbackList.current.push(callback)}useEffect(() => { myCallbacksList.current.forEach((callback) => callback()) myCallbacksList.current = [];}, [state]);…setStateWithCallback(newState, myCallback)
That’s it. Cheers!