Running a useEffect once with happy ESLint

Viewed 646

This is a React style question.

TL;DR Take the set function from React's useState. If that function "changed" every render, what's the best way to use it in a useEffect, with the Effect running only one time?

Explanation We have a useEffect that needs to run once (it fetches Firebase data) and then set that data in application state.

Here is a simplified example. We're using little-state-machine, and updateProfileData is an action to update the "profile" section of our JSON state.


const MyComponent = () => {
    const { actions, state } = useStateMachine({updateProfileData, updateLoginData});
    useEffect(() => {
        const get_data_async = () => {
            const response = await get_firebase_data();
            actions.updateProfileData( {user: response.user} );
        };
        get_data_async();
    }, []);

    return (
        <p>Hello, world!</p>
    );
}

However, ESLint doesn't like this: React Hook useEffect has a missing dependency: 'actions'. Either include it or remove the dependency array

Which makes sense. The issue is this: actions changes every render -- and updating state causes a rerender. An infinite loop.

Dereferencing updateProfileData doesn't work either.

Is it good practice to use something like this: a single-run useEffect?

Concept code that may / may not work:

const useSingleEffect = (fxn, dependencies) => {
    const [ hasRun, setHasRun ] = useState(false);
    useEffect(() => {
        if(!hasRun) {
            fxn();
            setHasRun(true);
        }
    }, [...dependencies, hasRun]);
};


// then, in a component:

const MyComponent = () => {
    const { actions, state } = useStateMachine({updateProfileData, updateLoginData});
    useSingleEffect(async () => {
        const response = await get_firebase_data();
        actions.updateProfileData( {user: response.user} );
    }, [actions]);

    return (
        <p>Hello, world!</p>
    );
}

But at that point, why even care about the dependency array? The initial code shown works and makes sense (closures guarantee the correct variables / functions), ESLint just recommends not to do it.

It's like if the second return value of React useState changed every render:

const [ foo, setFoo ] = useState(null);
//             ^ this one

If that changed every render, how do we run an Effect with it once?

1 Answers

Ignore the eslint rule for line

If you truly want the effect to run only once exactly when the component mounts then you are correct to use an empty dependency array. You can disable the eslint rule for that line to ignore it.

useEffect(() => {
  const get_data_async = () => {
    const response = await get_firebase_data();
    actions.updateProfileData( {user: response.user} );
  };
  get_data_async();

  // NOTE: Run effect once on component mount, please
  // recheck dependencies if effect is updated.
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

Note: If you later update the effect and it needs to run after other dependencies then this disabled comment can potentially mask future bugs, so I suggest leaving a rather overt comment as for the reason to override the established linting rule.

Custom hook logic

Alternatively you can use a react ref to signify the initial render. This is preferable to using some state to hold the value as updating it would trigger unnecessary render.

const MyComponent = () => {
  const { actions, state } = useStateMachine({updateProfileData, updateLoginData});
  const initialRenderRef = useRef(true);

  useEffect(() => {
    const get_data_async = () => {
      const response = await get_firebase_data();
      actions.updateProfileData( {user: response.user} );
    };

    if (initialRenderRef.current) {
      initialRenderRef.current = false;
      get_data_async();
    }
  }, [actions]); // <-- and any other dependencies the linter complains about

  return (
    <p>Hello, world!</p>
  );
}

And yes, absolutely you can factor this "single-run logic" into a custom hook if it is a pattern you find used over and over in your codebase.

Related