11
votes

There are several other SO questions on this where the answer is either to eliminate the dependencies complaints via ESLint (I'm using typescript) or to do something else to still allow the second parameter of useEffect to be []. However per the React docs this is not recommended. Also under the react useEffect docs it says

If you pass an empty array ([]), the props and state inside the effect will always have their initial values. While passing [] as the second argument is closer to the familiar componentDidMount and componentWillUnmount mental model, there are usually better solutions to avoid re-running effects too often. Also, don’t forget that React defers running useEffect until after the browser has painted, so doing extra work is less of a problem.

I have the following code:

  useEffect(() => {
    container.current = new VisTimeline(container.current, items, groups, options);
  }, [groups, items, options]);

I want it to run only one time.

Is the only way around this to let it run each time and useState to track it has ran before like this:

  const [didLoad, setDidLoad] = useState<boolean>(false);

  useEffect(() => {
    if (!didLoad) {
      container.current = new VisTimeline(container.current, items, groups, options);
      setDidLoad(true);
    }
  }, [didLoad, groups, items, options]);
4

4 Answers

12
votes

The way I handle this now is to put the appropriate dependencies in the list of dependencies.

Because I want the effect to run only one time, and because the effect only relies on some data when the component first mounts, it's perfectly fine to omit those dependencies. For example, the groups prop may change later, but this effect doesn't need to run again.

But, as a habit I don't omit the recommended dependencies and I always list them. If I intentionally do omit something, I add a eslint ignore statement (Haven't run across the need to do that yet though)... it's whatever convention you want to follow as long as you understand what is happening when that data changes and the effect does / does not run.

However the code I proposed isn't the best solution either if you do want to list the dependencies:

 const [didLoad, setDidLoad] = useState<boolean>(false);

  useEffect(() => {
    if (!didLoad) {
      container.current = new VisTimeline(container.current, items, groups, options);
      setDidLoad(true);
    }
  }, [didLoad, groups, items, options]);

I don't want to cause a render when this effect runs. So instead of using state, I will use a ref (which doesn't need to be a dependency).

 const timelineLoaded = useRef<boolean>(false);

  useEffect(() => {
    if (!timelineLoaded.current) {
      container.current = new VisTimeline(container.current, items, groups, options);
      timelineLoaded.current = true;
    }
  }, [groups, items, options]);
0
votes

Adding extra code to work around tooling is not good.

Solve the actual problem - in this case exclude the code you know works the way you want from the linter.

Specifically disable linting on the code where you know you do not want values in the useEffect dependency array. Add this above the useEffect code block:

/* eslint-disable react-hooks/exhaustive-deps */
-1
votes
useEffect(() => {
    container.current = new VisTimeline(container.current, items, groups, options);
  }, [groups, items, options]);

The above code runs the function everytime one of the variables in the array changes. If you want to run this just once then [] should be the array as mentioned in the docs. So basically

useEffect(() => {
    container.current = new VisTimeline(container.current, items, groups, options);
  }, []);

Hope this helped.

-1
votes

This custom hook is a wrapper on useEffect, will run only after the first render and does not violate react-hooks/exhaustive-deps eslint rule:

import { useEffect } from 'react';

const useEffectOnlyOnce = func => useEffect(func, []);

export default useEffectOnlyOnce;

It can be used like this:

import useEffectOnlyOnce from '<path-to-your-custom-hook>/useEffectOnlyOnce'

...

  useEffectOnlyOnce(() => {
    container.current = new VisTimeline(container.current, items, groups, options);
  });

...

This solution comes from a comment on CSS Tricks post