5

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.

Victor
  • 13,914
  • 19
  • 78
  • 147

3 Answers3

4

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?

demkovych
  • 7,827
  • 3
  • 19
  • 25
3

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)
}
Dupocas
  • 20,285
  • 6
  • 38
  • 56
  • I cannot figure out how this works. If the dependency is a property, this does not seem to work – Victor Sep 23 '19 at 14:34
  • This will work for props, state, or any other value for `A` and `B` – Dupocas Sep 23 '19 at 14:37
  • Sorry for bothering you, but could you please explain this in a bit more detail? I read about this custom hook on the React documentation site but cannot say I understand it 100%. Does have to do with the way effects are dispatched? Thanks! – Victor Sep 23 '19 at 14:38
  • 1
    I'll pudate my answer – Dupocas Sep 23 '19 at 14:40
0

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;
};