Let’s talk about useEffects in React Hooks! I’m going to share with you 4 tips you should have in mind when using useEffect.

Use a useEffect for a single purpose

In React Hooks, you can have multiple useEffect functions. This is a great feature because, if we analyze how to write clean code, you’ll see that functions should serve a single purpose (much like how a sentence should communicate one idea only).

Splitting useEffects into short and sweet single-purpose functions also prevents unintended executions (when using the dependency array).

For example, let’s say you have varA that is unrelated to varB, and you want to build a recursive counter based on useEffect (with setTimeout) so let’s write some bad code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
function App() {
  const [varA, setVarA] = useState(0);
  const [varB, setVarB] = useState(0);
  // Don't do this!
  useEffect(() => {
    const timeoutA = setTimeout(() => setVarA(varA + 1), 1000);
    const timeoutB = setTimeout(() => setVarB(varB + 2), 2000);

    return () => {
      clearTimeout(timeoutA);
      clearTimeout(timeoutB);
    };
  }, [varA, varB]);

  return (
    <span>
      Var A: {varA}, Var B: {varB}
    </span>
  );
}

As you can see, a single change in any of the variables varA and varB will trigger an update in both variables. This is why this hook doesn’t work properly.

Since this is a short example, you may feel that it’s obvious, however, in longer functions with more code and variables I guarantee you will miss this. So do the right thing and split your useEffect.

In this case, it should become:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
function App() {
  const [varA, setVarA] = useState(0);
  const [varB, setVarB] = useState(0);
  // Correct way
  useEffect(() => {
    const timeout = setTimeout(() => setVarA(varA + 1), 1000);

    return () => clearTimeout(timeout);
  }, [varA]);

  useEffect(() => {
    const timeout = setTimeout(() => setVarB(varB + 2), 2000);

    return () => clearTimeout(timeout);
  }, [varB]);

  return (
    <span>
      Var A: {varA}, Var B: {varB}
    </span>
  );
}

Note: The code here is for example purposes only, which is meant to help you easily understand the problems of useEffect. Normally, when a variable depends on its’ previous state, the recommended way is to do setVarA(varA => varA + 1) instead.

Use custom hooks whenever you can

Let’s take the example above again. What if the variables varA and varB are completely independent?

In this case, we can simply create a custom hook to isolate each variable. This way, you get to know exactly what each function is doing to which variable.

Let’s build some custom hooks then!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function App() {
  const [varA, setVarA] = useVarA();
  const [varB, setVarB] = useVarB();

  return (
    <span>
      Var A: {varA}, Var B: {varB}
    </span>
  );
}

function useVarA() {
  const [varA, setVarA] = useState(0);

  useEffect(() => {
    const timeout = setTimeout(() => setVarA(varA + 1), 1000);

    return () => clearTimeout(timeout);
  }, [varA]);

  return [varA, setVarA];
}

function useVarB() {
  const [varB, setVarB] = useState(0);

  useEffect(() => {
    const timeout = setTimeout(() => setVarB(varB + 2), 2000);

    return () => clearTimeout(timeout);
  }, [varB]);

  return [varB, setVarB];
}

Now each variable has its’ own hook. Much more maintainable and easy to read!

Conditionally run useEffect the right way

On the topic of setTimeout, let’s take the following example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function App() {
  const [varA, setVarA] = useState(0);

  useEffect(() => {
    const timeout = setTimeout(() => setVarA(varA + 1), 1000);

    return () => clearTimeout(timeout);
  }, [varA]);

  return <span>Var A: {varA}</span>;
}

For some reason, you want to limit the counter to a maximum of 5. There’s the correct way and the incorrect way.

Let’s take a look at the incorrect way first:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function App() {
  const [varA, setVarA] = useState(0);

  // Don't do this!
  useEffect(() => {
    let timeout;
    if (varA < 5) {
      timeout = setTimeout(() => setVarA(varA + 1), 1000);
    }

    return () => clearTimeout(timeout);
  }, [varA]);

  return <span>Var A: {varA}</span>;
}

Although this works, bear in mind that clearTimeout will run on any change of varA, while setTimeout is conditionally run.

The recommended way to run useEffect conditionally is to do a conditional return at the beginning of the function, like so:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function App() {
  const [varA, setVarA] = useState(0);

  useEffect(() => {
    if (varA >= 5) return;

    const timeout = setTimeout(() => setVarA(varA + 1), 1000);

    return () => clearTimeout(timeout);
  }, [varA]);

  return <span>Var A: {varA}</span>;
}

This is what you see Material UI using (as well as many others) and it makes sure you’re not running useEffect by mistake.

Type out every prop inside useEffect in the dependency array

If you’re using ESLint, then you’ve probably seen a warning that comes from ESLint exhaustive-deps rule.

This is crucial. When your app grows bigger and bigger, more dependencies (props) get added into each useEffect. To keep track of all of them and avoid stale closures, you should add each and every dependency into the dependency array. (Here’s the official take on this subject)

Again, on the topic of setTimeout, let’s say you want to run setTimeout only once and add on to varA, much like the previous examples.

You might be tempted to do the following incorrect example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function App() {
  const [varA, setVarA] = useState(0);

  useEffect(() => {
    const timeout = setTimeout(() => setVarA(varA + 1), 1000);

    return () => clearTimeout(timeout);
  }, []); // Avoid this: varA is not in the dependency array!

  return <span>Var A: {varA}</span>;
}

Although this will do what you want, let’s take a moment to think, “what if your code gets bigger?”, or, “what if I want to change the code above to something else?”

In that case, you’ll want to have the variables all mapped out, as it will be much easier to test and detect problems that might arise (like stale props and closures).

The correct way should be:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function App() {
  const [varA, setVarA] = useState(0);

  useEffect(() => {
    if (varA > 0) return;

    const timeout = setTimeout(() => setVarA(varA + 1), 1000);

    return () => clearTimeout(timeout);
  }, [varA]); // Great, we have our dependency array correctly set

  return <span>Var A: {varA}</span>;
}

That’s it, folks. If you have any questions or suggestions, I’m all ears! Reply or comment below and I’ll be sure to check it out!