skip to Main Content

I’m an experienced programmer (10+ years) but new to React (< 2 years). I picked up React in a very "operational" way (i.e. being happy when things work without very deep understanding of its inner workings). Now I’m trying to correct that.

One thing that surprises me is how easily it lets you do the wrong thing. ESLint might warn you, but the fix might be completely misleading. Here’s one situation in which I’m trying to figure out what the best practice is.

function MyComponent() {
  const [functionA, functionB] = useMyHooks();

  const shouldOnlyCallOnce = useCallback(() => {
    functionA();
    functionB();
  }
  []);

  useEffect(() => {
    shouldOnlyCallOnce();
  }, []);
  ...
}

Now ESLint will complain that functionA and functionB are not in the dependency array and suggest that I either (a) add them to the dependency array, or (b) remove the dependency array. I think both of these suggestions are wrong, and I’m not sure what the best practice is here.

Here’s what I think:

(a) If I want to code defensively, I should not make any assumptions about when functionA and functionB are re-defined. They might be redefined on every rerender. So to go this route, ESLint should also suggest that I memoize them first.

(b) Removing the dependency array would have the opposite effect from what I want, which is to run shouldOnlyCallOnce a single time.

So what’s the right pattern here? Do (a) with additional memoization? (But I also heard that excessive memoization can degrade performance).

2

Answers


  1. Both ESLint suggestions aren’t ideal. The best approach would be to memoize functionA and functionB(if they change or rerender) and then add them to the dependency array. This way, your callback stays stable and runs as intended without over-memoizing.

    Login or Signup to reply.
  2. According to the ESLint React hooks rules then your option (A) is correct, both functionA and functionB are external dependencies (to the hook) and should be included in the useCallback dependency array such that they are re-enclosed in callback scope when their references change. Option (B) and omitting the dependency array entirely is just about wrong 100% of the time and would be exactly the same as not using useCallback at all. The idea here is to provide stable callback references as much as possible.

    function MyComponent() {
      const [functionA, functionB] = useMyHooks();
    
      const shouldOnlyCallOnce = useCallback(() => {
        functionA();
        functionB();
      }
      [functionA, functionB]);
      ...
    }
    

    This creates shouldOnlyCallOnce as a stable memoized callback function to be used/called further down the line, e.g. passing shouldOnlyCallOnce as a stable reference as props to children components.

    Removing the dependency array would have the opposite effect from what
    I want, which is to run shouldOnlyCallOnce a single time.

    If you only want functionA and functionB to be called once, immediately then this sounds more like you want to just call them as intentional side-effects right then and there in MyComponent. If this is the case then you should instead use the useEffect hook.

    Examples:

    • Empty dependency array means invoke the useEffect hook callback exactly once when the component mounts.

      function MyComponent() {
        const [functionA, functionB] = useMyHooks();
      
        useEffect(() => {
          functionA();
          functionB();
        }
        []);
        ...
      }
      
    • Include functionA and functionB as dependencies when you wish for both of them to be called when either changes references:

      function MyComponent() {
        const [functionA, functionB] = useMyHooks();
      
        useEffect(() => {
          functionA();
          functionB();
        }
        [functionA, functionB]);
        ...
      }
      

    Ideally your useMyHooks should also already be using useCallback to memoize and provide stable functionA and functionB references to its consumers, otherwise you force all consumers to do this each time they want, or care, for them to also be stable references.

    const useMyHooks = () => {
      // Create memoized callbacks
      const functionA = useCallback(() => { ... }, [/* A dependencies */]);
      const functionB = useCallback(() => { ... }, [/* B dependencies */]);
    
      // Return memoized array of callbacks
      return useMemo(() => {
        return [functionA, functionB];
      }, [functionA, functionB]);
    };
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search