React hook useEffect dependency array

JavascriptReactjsReact Hooks

Javascript Problem Overview


I trying to wrap my head around the new hooks api of react. Specifically, I'm trying to construct the classic use case that once was the following:

componentDidUpdate(prevProps) {
    if (prevProps.foo !== this.props.foo) {
        // animate dom elements here...
        this.animateSomething(this.ref, this.props.onAnimationComplete);
    }
}

Now, I tried to build the same with a function component and useEffect, but can't figure out how to do it. This is what I tried:

useEffect(() => {
    animateSomething(ref, props.onAnimationComplete);
}, [props.foo]);

This way, the effect is only called when props.foo changes. And that does work – BUT! It appears to be an anti-pattern since the eslint-plugin-react-hooks marks this as an error. All dependencies that are used inside the effect should be declared in the dependencies array. So that means I would have to do the following:

useEffect(() => {
    animateSomething(ref, props.onAnimationComplete);
}, [props.foo, ref, props.onAnimationComplete]);

That does not lead to the linting error BUT it totally defeats the purpose of only calling the effect when props.foo changes. I don't WANT it to be called when the other props or the ref change.

Now, I read something about using useCallback to wrap this. I tried it but didn't get any further.

Can somebody help?

Javascript Solutions


Solution 1 - Javascript

I would recommend writing this as follows:

const previousFooRef = useRef(props.foo);

useEffect(() => {
    if (previousFooRef.current !== props.foo) {
       animateSomething(ref, props.onAnimationComplete);
       previousFooRef.current = props.foo;
    }
}, [props.foo, props.onAnimationComplete]);

You can't avoid the complexity of having a condition inside the effect, because without it you will run your animation on mount rather than just when props.foo changes. The condition also allows you to avoid animating when things other than props.foo change.

By including props.onAnimationComplete in the dependencies array, you avoid disabling the lint rule which helps ensure that you don’t introduce future bugs related to missing dependencies.

Here's a working example:

Edit animate

Solution 2 - Javascript

Suppress the linter because it gives you a bad advice. React requires you to pass to the second argument the values which (and only which) changes must trigger an effect fire.

useEffect(() => {
    animateSomething(ref, props.onAnimationComplete);
}, [props.foo]); // eslint-disable-line react-hooks/exhaustive-deps

It leads to the same result as the Ryan's solution.

I see no problems with violating this linter rule. In contrast to useCallback and useMemo, it won't lead to errors in common case. The content of the second argument is a high level logic.

You may even want to call an effect when an extraneous value changes:

useEffect(() => {
    alert(`Hi ${props.name}, your score is changed`);
}, [props.score]);

Solution 3 - Javascript

Move the values, that must be fresh (not stale) in the callback but mustn't refire the effect, to refs:

const elementRef = useRef(); // Ex `ref` from the question
const animationCompleteRef = useRef();

animationCompleteRef.current = props.onAnimationComplete;

useEffect(() => {
    animateSomething(elementRef, animationCompleteRef.current);
}, [props.foo, elementRef, animationCompleteRef]);

It works because useRef return value doesn't change on renders.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionoverdub60View Question on Stackoverflow
Solution 1 - JavascriptRyan CogswellView Answer on Stackoverflow
Solution 2 - JavascriptFinesseView Answer on Stackoverflow
Solution 3 - JavascriptFinesseView Answer on Stackoverflow