When I asked a few of my React front-end friends about the mental burden caused by React Hooks, I was surprised to hear that they didn’t feel the mental burden caused by React Hooks.

I’m suddenly a little autistic, you know? Is it me?

I have to say that the Use of React Hooks makes React developers a lot easier. However, in the actual use process, I did find that it brought a series of convenience, but also brought me a lot of trouble and discomfort.

React’s other wheels (Redux, Mobox, Dva, etc.) either solve only a part of the problem or introduce new problems while solving a problem. In short, they don’t make you feel clean and efficient.

Here are some of the mental burdens I encountered using React Hooks in my actual project, for your reference.

Trap 1: Referencing old variables

Let’s start with a simple use example of Hooks.

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() = > {
    const id = setInterval(() = > {
      setCount(count + 1);
    }, 1000);
    return () = > clearInterval(id); } []);return <h1>{count}</h1>;
}
Copy the code

The function is very simple, register a timer when the component is installed, add 1 per second, and then remove the timer when the component is uninstalled.

At first glance, it looks fine, but in reality, the page always displays a 1. This is because the component creates a new variable count each time it renders, but the useEffect function is a closure that references the count variable as it was first rendered (the value is always 1).

To solve this problem, it is easy to call the Api using the state-changing function: change setCount(count + 1) to setCount(count => count + 1).

However, if the useEffect function is slightly more complex and refers to multiple state dependencies, this approach cannot be used:

function Counter() {
  const [count, setCount] = useState(0);
  const [varA, setVarA] = useState(' ');
  

  useEffect(() = > {
    const id = setInterval(() = > {
        if (varA === 'xxx') {
            setCount(count + 1);
        } else {
            setCount(count + 2); }},1000);
    return () = > clearInterval(id); } []);/ /...
}
Copy the code

So what to do?

Replace useState with useRef? UseRef has a problem with variable references, but changing ref values does not re-render components and does not update the interface. Pass!

There are solutions, of course.

The first is to use the useReducer, which will not be used as an example here. This problem can be solved, but if the dispatch takes parameters, it will return to the problem itself.

Second, use the useEffect dependent array:

function Counter() {
  const [count, setCount] = useState(0);
  const [varA, setVarA] = useState(' ');
  

  useEffect(() = > {
    const id = setInterval(() = > {
        if (varA === 'xxx') {
            setCount(count + 1);
        } else {
            setCount(count + 2); }},1000);
    return () = > clearInterval(id);
  }, [count, varA]);

  / /...
}
Copy the code

A new problem with this approach, however, is that each time count and varA change, the timing function is recreated, which works just fine in this example, but is problematic in some scenarios, as discussed later.

Trap # 2: useEffect or not?

import { showTip } from 'tip';

function ExampleTip() {
    const [varA, setVarA] = useState(' ');
    const [varB, setVarB] = useState(' ');
    
    useEffect(() = > {
        showTip({ a: varA, b: varB })
    }, [varA]);
}
Copy the code

This type of requirement, which is common in development, is that when one state (varA) changes, we need to do something (in this case a tip box), but to do so we need to refer to other states (varB, etc.).

But when varB changes, we should not pop up the tip box.

In this case, varB should not be added to the dependency array.


Another case:

import { fetch } from 'api';

function Example() {
    const [varA, setVarA] = useState(' ');
    const [varB, setVarB] = useState(' ');
    
    useEffect(() = > {
        window.addEventListener('click'.() = > {
            doSomeThing({ a: varA, b: varB }) 
        });
        return () = > {
            // remove listener
        }
    }, [varA,varB]);
}
Copy the code

In this case, useEffect, we want to use the latest value of varB instead of the value at the time useEffect was called. However, the variable referenced in useEffect is always the value when varA changes, and useEffect is not known after varB changes.

In this case, we need to add varB to the dependency array and listen again when varB changes.


For very simple code, we can usually figure out what to add and what not to add. But every time we write, we have to think about what to add and what not to add, which undoubtedly increases the mental burden.

Also, without the guarantee of a tool, it’s easy to get things wrong, which is why we need tools like ESLint and typescript.

The authorities are aware of this problem and have provided us with the corresponding ESLint plugin: eslint-plugin-react-hooks.

The real problem is that for any state variable, there are two cases where you should and shouldn’t add a dependency array, and the tools can’t tell the difference at the code level!

What to do? The official recommendation is to enable the exhaustive deps rule, which means that any state variable referenced in useEffect (except ref) should be included in the dependent array!

It is also stated in the official documentation that the second parameter may be added automatically at build time in future versions.

This means that adding all variables used in useEffect to the dependency array seems to be the official recommendation and the most correct choice. And if you want to use esLint-related tools, use this approach as well.

This solves the problem of referring to old variables and saves us from wondering if we should add dependent arrays.

But new problems were introduced, such as the showTip one above, where we had to add some extra code:

import { showTip } from 'tip';

function ExampleTip() {
    const [varA, setVarA] = useState(' ');
    const [varB, setVarB] = useState(' ');
    
    const varBRef = useRef(varB);
    varBRef.current = varB;
    
    useEffect(() = > {
        showTip({ a: varA, b: varBRef.current })
    }, [varA]);
}
Copy the code

The useRef variable does not need to be added to the dependent array because the reference does not change.

But what if useEffect references multiple variables? Wouldn’t it be a hassle to add a ref to each of them?

I usually use this method:

function ExampleTip() {
    const [varA, setVarA] = useState(' ');
    const [varB, setVarB] = useState(' ');
    
    const showTipRef = useRef(null);
    showTipRef.current = () = > {
        showTip({ a: varA, b: varBRef.current });
    }
    
    useEffect(() = > {
        showTipRef.current && showTipRef.current();
    }, [varA]);
}
Copy the code

The problem was solved, but it didn’t work.

Trap 3: Did the quote change?

For the same component, we can define useRef variables without adding dependency arrays, because the ESLint plugin recognizes that this is an immutable reference.

However, look at the following component:

function Example(props) {
    const { id, fetch } = props;
    
    useEffect(() = > {
        fetch(id);
    }, [id, fetch]);
}
Copy the code

We need to execute the fetch method when the ID changes. However, when the fetch reference changes, we generally do not need to fetch again.

Even if the fetch reference passed in is immutable, the ESLint plugin does not recognize it, so it still requires that the FETCH be added to the dependency array.

So, at a glance at the component, can you tell if fetch is going to change or not? Can you guarantee that the fetch only happens when the ID changes?

It looks like it can run, but I don’t trust it.

Pitfall 4: The risk of a loop introduced by dependency

function Child(props) {
    const { onAppear, onLeave } = props;
    useEffect(() = > {
        onAppear();
        return () = > {
            onLeave();
        }
    }, [onAppear, onLeave])
}

function Parent() {
    const [count, setCount] = useState(0);
    const appearItem = () = > {
        setCount(count + 1);
    }
    const leaveItem = () = > {
        setCount(count - 1);
    }
    
    return (
        <>
            <Child onAppear={appearItem} onLeave={leaveItem} />
            <Child onAppear={appearItem} onLeave={leaveItem} />
        </>
    );
}
Copy the code

In the example above, it seems fine to open each component individually, but it runs in an infinite loop. Because the onAppear, onLeave references in the Child component change every time you render!

To solve this problem, we have to wrap appearItem, leaveItem via useCallback:

function Child(props) {
    const { onAppear, onLeave } = props;
    useEffect(() = > {
        onAppear();
        return () = > {
            onLeave();
        }
    }, [onAppear, onLeave])
}

function Parent() {
    const [count, setCount] = useState(0);
    const appearItem = useCallback(() = > {
        setCount(count + 1);
    }, [count]);
    const leaveItem = useCallback(() = > {
        setCount(count - 1);
    }, [count]);
    
    return (
        <>
            <Child onAppear={appearItem} onLeave={leaveItem} />
            <Child onAppear={appearItem} onLeave={leaveItem} />
        </>
    );
}
Copy the code

It looks like it’s fixed, but it’s still an infinite loop because count changes every time you render! This is where useRef comes in…

This kind of invisible loop risk is actually common in hooks code. To avoid these risks, we have to think a lot.

Trap # 5: Misleading metrics

function Component(props) {
    const varA = useRef(props.a);
    const [varB] = useState(props.b);
}
Copy the code

What’s wrong with this simple component?

At first glance, the value of varA, varB is evaluated dynamically against the value of props, because each time useRef is rendered, useState is called once, and props. A and props. B are passed once.

But in fact React only uses the value passed in the first time!

This may not matter much to the hooks functions that are often used, such as useRef, useState, because you’ve been paying attention and already know how they work instinctively.

But what about a custom hooks?

function Component() {
    const [value, setValue] = useState(' ');
    const test = useCustomHook(value);
}
Copy the code

So this vluae, is the first value going to be used, or are all values going to be used?

conclusion

I’m so tired of writing! The examples described above are the problems I often encounter in my actual work. There are other problems, many of which can’t be described in a few words.

This is only according to my level of understanding, some of which may not be particularly correct, welcome criticism and correction!

Anyway, in my personal opinion, React Hooks annoy me mainly on the following points:

  • Counterintuitive: The way code actually works is very different from what it looks like at first glance.
  • A lot to think about: A lot of times, we have to think about issues that shouldn’t be on our minds, but should be addressed at the framework level.
  • Insecurity: Even if you put a lot of thought into writing components, there are times when you look back at the code you wrote and feel insecure.

When you think about it, the root cause of all the problems is that the React function is limited by the component mechanism: every time a component is rendered, all the code in the component is called again.

Vue’s combinational Api is similar to React hooks, but it doesn’t have that much to worry about because its setup only executes once in the lifetime of the component.