useCallback() dependency array vs constant-reference callback - reactjs

As per React official documentation, if a component new state is computed using the previous state, one can pass a function to setState. The function will receive the previous value, and return an updated value.
Consider the following code snippet example:
const [counter, setCounter] = useState(0);
const btnClick = useCallback(() => setCounter(counter+1), [counter]); //(1)
const btnClick = useCallback(() => setCounter(previous => previous+1), []);//(2)
btnClick is passed as a callback function for the 'onClick' event of a rendered button.
In (1), the btnClick function reference is updated each time the counter state variable is modified.
In (2), the btnClick function reference is created during the first render cycle and is cached for the rest of the component lifetime; at this level, the state update function will receive the previous state value and return an updated value.
At this level, I have 2 questions:
1- As a good practice, should I opt for (1) or (2)? And are there any advantages for using (1) instead of (2)?
2- Does (2) apply to both primitive and reference types? (check below, I switched the state from being an Integer to an Object)
const [counter, setCounter] = useState({cntr:1});
const btnClick = useCallback(() => setCounter({...counter,cntr:counter.cntr+1}), [counter]); //(1)
const btnClick = useCallback(() => setCounter(previous => return {...previous ,cntr:previous.cntr+1}), []);//(2)
Does "previous" in (2) still reference the previous state value or does it refer to a stale state that was cached during the component first render cycle?
Your help is appreciated.

So, it is a deep question, let's try to explain how it works.
useCallback just promise you that btnClick will not reinitialize. This does not related to useState and body of function at all.
In react community we do not have any good practices on this point. The difference is when you call setState react promise you that sometimes value will be updated.
For example:
setValue(1);
setValue(2);
setValue(3);
It does not mean react will update the state three times. React can combine it to one update and set just last value.
Pass function as a argument to setState
const myFunc = (oldValue) => ({})
useValue(myFunc)
it means that react sees you pass function then all useValue will be called immediately and after useValue(myFunc) will be called.
Developers need it to work with last data which is placed in the state.
Does "previous" in (2) still reference the previous state value or
does it refer to a stale state that was cached during the component
first render cycle?
So, as a result of the question previous in setState will be recent value from state.

Related

Directly using several states included in useEffect's dependency array

Considering an useEffect with 2 different states in the dependency array. The useEffect hook will run whenever any of those two states are updated, but if i update one of them, will i have access to the lastest value of the other inside useEffect? And if not, what is the best approach to it?
function Component() {
const [state1, setState1] = useState('');
const [state2, setState2] = useState('');
useEffect(() => {
console.log(state1, state2)
}, [state1, state2]);
return <>...</>
}
The callback inside useEffect will run after the render conditionally based on dependency array.
If your state values are updated in the same render cycle then they are batched (by React) and the next render cycle will show both the correct values in the useEffect callback.
If you only update any one of them, you do not have to worry about the other value because the callback in useEffect will be using the recently updated value of the other variable too.
Note: The only time you might face an issue is when you have stale state values because of closure, but that is a specific case.

Init UseState value from UseContext value

I have context and state variables. My state variable is initialized with my context variable. When I update my context in another component for exemple: changing the player's action (attack to defend), state variable keep the previous value.
const [player,setPlayer] = useContext(PlayerContext);
const [action, setAction] = useState(player.action);
useEffect(() => {
console.log(action); // => attack
console.log(player.action); // => defend
});
This must surely be a rendering problem.
If you want the action state variable to reflect updates to player.action, you need make sure your dependency array (the second parameter to useEffect) includes player.action (so that it only runs when the value changes), and then call setAction explicitly:
useEffect(() => {
if (player.action !== action) {
setAction(player.action);
}
}, [player.action])
The React documentation has an extensive guide on Using the Effect Hook that might be helpful, along with the useEffect hook API reference.
Your context state is changed somewhere after your component is rendered. Therefore the component state is not in sync with the context state anymore. With your browsers debugger you can check this behaviour and find out where it is changed.
As a general rule you should avoid adding a component state, when it shall only mirror a context‘s state, consume the context directly inside your component.

React Hook: setState usage

What's the difference between
1
const [state, setState] = useState(0)
setState(state+1)
2
const [state, setState] = useState(0)
setState(...prevState => prevState+1)
In the first option, based on the documentation:
The setState function is used to update the state. It accepts a new state value and enqueues a re-render of the component.
In the second option, called functional update:
If the new state is computed using the previous state, you can pass a function to setState. The function will receive the previous value, and return an updated value.
So basically if you'd like to be sure your state will be updated based on the previous state, you need to use the second option.
Read further in the official documentation of useState.
I hope this clarifies!

React hooks useState when setting array is not keeping old state [duplicate]

What's the difference between
1
const [state, setState] = useState(0)
setState(state+1)
2
const [state, setState] = useState(0)
setState(...prevState => prevState+1)
In the first option, based on the documentation:
The setState function is used to update the state. It accepts a new state value and enqueues a re-render of the component.
In the second option, called functional update:
If the new state is computed using the previous state, you can pass a function to setState. The function will receive the previous value, and return an updated value.
So basically if you'd like to be sure your state will be updated based on the previous state, you need to use the second option.
Read further in the official documentation of useState.
I hope this clarifies!

More advanced comparison in React's useEffect

I am looking for a way to perform more advanced comparison instead of the second parameter of the useEffect React hook.
Specifically, I am looking for something more like this:
useEffect(
() => doSomething(),
[myInstance],
(prev, curr) => { /* compare prev[0].value with curr[0].value */ }
);
Is there anything I missed from the React docs about this or is there any way of implementing such a hook on top of what already exists, please?
If there is a way to implement this, this is how it would work: the second parameter is an array of dependencies, just like the useEffect hook coming from React, and the third is a callback with two parameters: the array of dependencies at the previous render, and the array of dependencies at the current render.
you could use React.memo function:
const areEqual = (prevProps, nextProps) => {
return (prevProps.title === nextProps.title)
};
export default React.memo(Component, areEqual);
or use custom hooks for that:
How to compare oldValues and newValues on React Hooks useEffect?
In class based components was easy to perform a deep comparison. componentDidUpdate provides a snapshot of previous props and previous state
componentDidUpdate(prevProps, prevState, snapshot){
if(prevProps.foo !== props.foo){ /* ... */ }
}
The problem is useEffect it's not exactly like componentDidUpdate. Consider the following
useEffect(() =>{
/* action() */
},[props])
The only thing you can assert about the current props when action() gets called is that it changed (shallow comparison asserts to false). You cannot have a snapshot of prevProps cause hooks are like regular functions, there aren't part of a lifecycle (and don't have an instance) which ensures synchronicity (and inject arguments). Actually the only thing ensuring hooks value integrity is the order of execution.
Alternatives to usePrevious
Before updating check if the values are equal
const Component = props =>{
const [foo, setFoo] = useState('bar')
const updateFoo = val => foo === val ? null : setFoo(val)
}
This can be useful in some situations when you need to ensure an effect to run only once(not useful in your use case).
useMemo:
If you want to perform comparison to prevent unecessary render calls (shoudComponentUpdate), then useMemo is the way to go
export React.useMemo(Component, (prev, next) => true)
But when you need to get access to the previous value inside an already running effect there is no alternatives left. Cause if you already are inside useEffect it means that the dependency it's already updated (current render).
Why usePrevious works
useRef isn't just for refs, it's a very straightforward way to mutate values without triggering a re render. So the cycle is the following
Component gets mounted
usePrevious stores the inital value inside current
props changes triggering a re render inside Component
useEffect is called
usePrevious is called
Notice that the usePrevious is always called after the useEffect (remember, order matters!). So everytime you are inside an useEffect the current value of useRef will always be one render behind.
const usePrevious = value =>{
const ref = useRef()
useEffect(() => ref.current = value,[value])
}
const Component = props =>{
const { A } = props
useEffect(() =>{
console.log('runs first')
},[A])
//updates after the effect to store the current value (which will be the previous on next render
const previous = usePrevious(props)
}
I hit the same problem recently and a solution that worked for me is to create a custom useStateWithCustomComparator hook.
In your the case of your example that would mean to replace
const [myInstance, setMyInstance] = useState(..)
with
const [myInstance, setMyInstance] = useStateWithCustomComparator(..)
The code for my custom hook in Typescript looks like that:
const useStateWithCustomComparator = <T>(initialState: T, customEqualsComparator: (obj1: T, obj2: T) => boolean) => {
const [state, setState] = useState(initialState);
const changeStateIfNotEqual = (newState: any) => {
if (!customEqualsComparator(state, newState)) {
setState(newState);
}
};
return [state, changeStateIfNotEqual] as const;
};

Resources