1

The question is:

Assuming I have a fairly extensive logic in the component. Should I put all this logic inside useEffects or should I move these functions outside useEffect and use useCallback?

I can't find any peer-reviewed approach to this topic.

Below is an example of what I mean:

  1. functions inside useEffect approach
  useEffect(() => {
    const fun1 = () => {
      /**
       * 50 lines of code
       */
    }
    fun1()
  }, [var1, var2])

  useEffect(() => {
    const fun2 = () => {
      /**
       * 50 lines of code
       */
    }
    fun2()
  }, [var3, var4])
  1. functions outside useEffect approach
  const fun1 = useCallback(() => {
    /**
     * 50 lines of code
     */
  }, [var1, var2])

  const fun2 = useCallback(() => {
    /**
     * 50 lines of code
     */
  }, [var3, var4])

  useEffect(() => {
    fun1()
  }, [fun1])

  useEffect(() => {
    fun2()
  }, [fun2])

Let's assume it's even more complicated. Which approach is preferred?

Jacek Tessen
  • 43
  • 1
  • 7
  • does it make difference whether u put them inside or outside? if so how? – Sami Ullah Dec 17 '21 at 11:05
  • I think both approaches don't make much difference from the functional side. The difference is readability. I don't know if there is anything different in terms of performance. From my point of view, smaller `useEffect` results in better readability. If only because I can quickly see how much `useEffect` is in total and I can define all functions side by side, above `useEffect` – Jacek Tessen Dec 17 '21 at 11:45
  • [Dylan gave you some good generic advice](https://stackoverflow.com/a/70392398/438273), but it's not possible to give you advice for your case because you haven't shown what your functions do. It's obvious that they're closures around component state, but opaque beyond that. Depending on what they do and what kinds of refactors you're willing to make (e.g. changing params, return values, etc.) there is more than one optimized approach, not even considering DX. – jsejcksn Dec 17 '21 at 12:05

2 Answers2

2

There are many ways to refactor a closure within the effect hook. Because you haven't shown your function code, consider the common example of the counter component:

Given that the following utility function is in scope...

function isSingular (n: number): boolean {
  return Math.abs(n) === 1;
}
function Example (): React.ReactElement {
  const [count, setCount] = useState(0);
  const [message, setMessage] = useState('');

  useEffect(() => {
    const updateMessage = () => {
      setMessage(`You clicked ${count} time${isSingular(count) ? '' : 's'}`);
    };
    updateMessage();
  }, [count, setMessage]);

  return (
    <div>
      <div>{message}</div>
      <button onClick={() => setCount(n => n + 1)}>Increment</button>
    </div>
  );
}

The component uses two state variables, count and message (which is computed from count). A closure updateMessage is defined in the effect hook's callback which encapsulates count and setMessage. There are many ways to refactor this:

Important: No matter which method you use, if it involves the use of a dependency array, make sure the list of dependencies is correct and exhaustive.


You can define the closure outside the effect hook and pass it directly as the callback (this is the one you asked about):

function Example (): React.ReactElement {
  const [count, setCount] = useState(0);
  const [message, setMessage] = useState('');

  const updateMessage = () => {
    setMessage(`You clicked ${count} time${isSingular(count) ? '' : 's'}`);
  };

  useEffect(updateMessage, [count, setMessage]);

  return // the same JSX...
}

<div id="root"></div><script src="https://unpkg.com/react@17.0.2/umd/react.development.js"></script><script src="https://unpkg.com/react-dom@17.0.2/umd/react-dom.development.js"></script><script src="https://unpkg.com/@babel/standalone@7.16.6/babel.min.js"></script><script>Babel.registerPreset('tsx', {presets: [[Babel.availablePresets['typescript'], {allExtensions: true, isTSX: true}]]});</script>
<script type="text/babel" data-type="module" data-presets="tsx,react">

// You'd use ESM:
// import ReactDOM from 'react-dom';
// import {default as React, useEffect, useState} from 'react';

// This snippet uses UMD:
const {useEffect, useState} = React;

function isSingular (n: number): boolean {
  return Math.abs(n) === 1;
}

function Example (): React.ReactElement {
  const [count, setCount] = useState(0);
  const [message, setMessage] = useState('');

  const updateMessage = () => {
    setMessage(`You clicked ${count} time${isSingular(count) ? '' : 's'}`);
  };

  useEffect(updateMessage, [count, setMessage]);

  return (
    <div>
      <div>{message}</div>
      <button onClick={() => setCount(n => n + 1)}>Increment</button>
    </div>
  );
}

ReactDOM.render(<Example />, document.getElementById('root'));

</script>

You can define it as an impure function rather than a closure:

function updateMessage (
  count: number,
  setString: React.Dispatch<React.SetStateAction<string>>,
): void {
  setString(`You clicked ${count} time${isSingular(count) ? '' : 's'}`);
};

function Example (): React.ReactElement {
  const [count, setCount] = useState(0);
  const [message, setMessage] = useState('');

  useEffect(() => updateMessage(count, setMessage), [count, setMessage]);

  return // the same JSX...
}

You can extract to a custom hook everything related to the message state:

function useMessage (count: number): string {
  return `You clicked ${count} time${isSingular(count) ? '' : 's'}`;
};

function Example (): React.ReactElement {
  const [count, setCount] = useState(0);
  const message = useMessage(count);

  return // the same JSX...
}

You can use the memo hook:

function Example (): React.ReactElement {
  const [count, setCount] = useState(0);
  const message = useMemo(() => `You clicked ${count} time${isSingular(count) ? '' : 's'}`, [count]);

  return // the same JSX...
}

Hopefully that gives you some perspective which you can then apply to your component's code.

jsejcksn
  • 27,667
  • 4
  • 38
  • 62
1

Assuming I have a fairly extensive logic in the component.

You should probably move the logic out of the component and into a hook since that is one of the main problems that hooks attempt to solve.

Aside from that, your second method introduces an additional dependency of useCallback. What do you gain from doing that? Not much. But a lot is lost (more complexity plus an additional dependency).

Dylan Kerler
  • 2,007
  • 1
  • 8
  • 23
  • From my point of view, I would prefer to declare all the functions in one place first and start using them below. But I understand what you mean. And I agree with you on the first part of the answer. If these questions start to arise, maybe there is too much logic in the component. – Jacek Tessen Dec 17 '21 at 11:49