Coding With Fun
Home Docker Django Node.js Articles Python pip guide FAQ Policy

React hooks online bugs after re-disking


Jun 01, 2021 Article blog


Table of contents


The article is reproduced from the public number: Front end dew

Recently, there were classmates on the team who caused some bugs by writing react hooks, and even one case was an online problem. T here have also been some arguments within the team over whether to write hooks or not. D o you want lint or not? D o you want to add autofix or not? The conclusion of the argument is as follows:

  1. Write or write;
  2. Be sure to learn hooks before you write them;
  3. The team produces another must-read document, and each student must be asked to read and write first.

So this article is available.

This article focuses on two main points:

  1. Hard requirements before writing hooks;
  2. Write a few common notes for hooks.

Hard requirements

1. The official React Hooks documentation must be read in its entirety once

Documents in English: https://reactjs.org/docs/hooks-intro.html

Chinese documents: https://zh-hans.reactjs.org/docs/hooks-intro.html

Highlights must-see hooks: useState useReducer useEffect useCallback useMemo also recommended to read:

  1. Dan's Complete Guide to UseEffect
  2. React Hooks Completely Hands-on Guide by Yan Liang's Classmates

2. The project must introduce the lint plug-in and turn on the rules

lint plug-in: https://www.npmjs.com/package/eslint-plugin-react-hooks must-open rules:

{
  "plugins": ["react-hooks"],
  "rules": {
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}

Among them, react-hooks/exhaustive-deps can be at least warn, or error. It is recommended that new projects be directly matched with "error" and historical projects with "warn".

Remember, this article is a hard condition.

If your project does not currently have hooks lint rule turned on, please do not write any hooks code. I f you cr code when you find the other side of the front-end project, do not open the rules, and submit hooks code, do not merge. This requirement is adapted to any react React project.

These two rules will prevent us from stepping on the pit. A lthough for hooks the process can be "painful." H owever, if you think these two rules are bothering you writing code, you're not fully aware of hooks.

If you really don't need "exhaustive-deps" for some // eslint-disable-next-line react-hooks/exhaustive-deps at the code

Remember that you can only ban this code, you can't be lazy to ban the whole file.

3. Do not global autofix if a warning caused by hooks-related lint is found

With the exception hooks a normal lint doesn't change the code logic at all, just adjusts the writing specification. H owever, the lint rules of hooks are different, and hooks in exhaustive-deps can cause code logic to change, which can easily cause online problems, so for hooks' waning do not do global autofix hooks Unless every logic is guaranteed to be fully regressed.

Another little sister added: eslint-plugin-react-hooks have cancelled exhaustive-deps autofix since version 2.4.0." Therefore, please try to upgrade the lint plug-in to the latest version of the project to reduce the risk of errors.

It is then recommended to turn on "autofix on save" for vscode In the future, whatever the problem, error and warning can be as close as possible in the initial development phase, to ensure that self-test and testing is in line with the rules of the code.

Common note points

Dependency issues

Dependency and closure issues are the core reasons why exhaustive-deps must be turned on. T he most common error is the binding event at mount, and subsequent status updates go wrong.

Example of an error code: (addEventListener is used here for onclick binding, just to illustrate the situation)

function ErrorDemo() {
  const [count, setCount] = useState(0);
  const dom = useRef(null);
  useEffect(() => {
    dom.current.addEventListener('click', () => setCount(count + 1));
  }, []);
  return <div ref={dom}>{count}</div>;
}

The initial idea of this code is that whenever the user dom count adds 1. T he ideal effect is to keep pointing and adding all the time. But the actual effect is that it doesn't add up after "1."

Let's sort it out, useEffect(fn, []) representative will only trigger when mounting. T his is also the first time that the fn executes once, binding the click event, and clicking trigger setCount(count + 1) At first glance, count or that count, will certainly always add ah, of course, the reality is crackling face.

What is the nature of state change trigger page rendering? T he essence is ui = fn(props, state, context) Props, internal state, and context changes can cause the render function (in this case, ErrorDemo) to be re-executed and then returned to the new view.

Now that the problem is, ErrorDemo is a function that has been executed many times, does count inside the first function have anything to do with the count several times later? I t doesn't matter if you think about it that way. S o why do you know for the second time that count is 1 instead of 0? I s the first setCount followed by the same function? T his involves some of the underlying principles of hooks, as well as why hooks declarations need to be declared at the top of the function and are not allowed in conditional statements. Not much is said here.

The conclusion is that each count is a re-declared variable, pointing to a completely new data; setCount

Back to the point, we know that every time we render, the internal count is actually a whole new variable. Then we bind the click event method, that is: setCount(count + 1) where count actually refers to the count at the first render, so it has always been 0, so setCount, has always been set to count to 1.

So how do you solve the problem?

First, you should comply with the previous hard requirements, you must add lint rules, and turn on autofix on save. T hen you'll find out that this effect is actually count-dependent. count autofix will help you automatically fill in the dependencies, and the code will look like this:

useEffect(() => {
  dom.current.addEventListener('click', () => setCount(count + 1));
}, [count]);

That's certainly not right, it's the equivalent of rebinding an event once every time count changes. So for event binding, or similar scenarios, there are several ideas that I prioritize by my general handling:

Idea 1: Eliminate dependency

In this scenario, quite simply, we mainly take advantage of another usage of setCount functional updates. It's good to write like this: () => setCount(prevCount => ++prevCount) don't care about what's new and old, what's closed, save your mind.

Idea 2: Rebind the event

So what if we're going to consume this count in this event? Like this:

dom.current.addEventListener('click', () => {
  console.log(count);
  setCount(prevCount => ++prevCount);
});

We don't have to stick to it only once on mount. Y ou can also remove the event before each re-render, and bind the event after the render. Here, using the features of useEffect, you can see the documentation for yourself:

useEffect(() => {
  const $dom = dom.current;
  const event = () => {
    console.log(count);
    setCount(prev => ++prev);
  };
  $dom.addEventListener('click', event);
  return () => $dom.removeEventListener('click', event);
}, [count]);

Idea 3:

If it's expensive, or it's cumbersome to write, it's a hassle to useRef use useRef which I personally don't like very much, but it solves the problem, as follows:

const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
  dom.current.addEventListener('click', () => {
    console.log(countRef.current);
    setCount(prevCount => {
      const newCount = ++prevCount;
      countRef.current = newCount;
      return newCount;
    });
  });
}, []);

UseCallback with useMemo

These two api in fact, are conceptually quite understandable, one is a "cache function" and the other is a cache "function return value". But we often don't bother to use it, and sometimes we sometimes use it wrong.

From the above dependency problem we can actually know that hooks are actually very sensitive to the "no change" point. I f an effect uses a data or method internally. I f we don't add it to our dependencies, it's easy to have closure problems that lead to data or methods that aren't the ones we ideally have. I f we add it, it's likely that they'll be executed crazy because of their effect If it's really developed, people should often encounter this kind of problem.

Therefore, it is recommended here:

  1. Inside the component, methods that become other useEffect dependencies are recommended to wrap with useCallback or write directly in useEffect that references it.
  2. If your function is passed as props to subcomponents, be sure to wrap them with useCallback which can be very troubling if each render causes a change in the function you pass. It is also not conducive to rendering optimization by react.

But there's another scenario that's easy to ignore, and it's easy to confuse useCallback with useMemo typically: throttle stabilization.

For example:

function BadDemo() {
  const [count, setCount] = useState(1);
  const handleClick = debounce(() => {
    setCount(c => ++c);
  }, 1000);
  return <div onClick={handleClick}>{count}</div>;
}

We want to prevent users from clicking continuously to trigger multiple changes, add stabilization, stop clicking for 1 second before count + 1 which is ideally OK. B ut the reality is bone-chilling, we have a lot of page components, and this BadDemo may be re-rendered because of what the parent does. Now if our page is re-rendered every 500 milliseconds, here's it:

function BadDemo() {
  const [count, setCount] = useState(1);
  const [, setRerender] = useState(false);
  const handleClick = debounce(() => {
    setCount(c => ++c);
  }, 1000);
  useEffect(() => {
    // 每500ms,组件重新render
    window.setInterval(() => {
      setRerender(r => !r);
    }, 500);
  }, []);
  return <div onClick={handleClick}>{count}</div>;
}

Each time the render handleClick to actually be a different function, this stabilization naturally fails. Such a situation for some of the defense focus requirements are particularly high scenes, there is a greater online risk.

So what do we do? Naturally you want to add useCallback :

const handleClick = useCallback(debounce(() => {
  setCount(c => ++c);
}, 1000), []);

Now we've found that the effect meets our expectations, but there's still a big hole hidden behind it.

What if this anti-shake function has some dependencies? F or setCount(c => ++c); becomes setCount(count + 1) T hen this function depends on count The code becomes something like this:

const handleClick = useCallback(
  debounce(() => {
    setCount(count + 1);
  }, 1000),
  []
);

You'll find that your lint rule doesn't even require you to populate the deps array with count as a dependency. T his, in turn, led to the initial problem, with only the first click of the count. Why is that?

Because the useCallback is passed in as an execution statement, not a function declaration. Just to say that it executes the new function returned later, we used it as a reference to the useCallback function, and this new function is exactly what it is, in fact, the lint rule does not know.

A more reasonable posture should be to use useMemo :

const handleClick = useMemo(
  () => debounce(() => {
    setCount(count + 1);
  }, 1000),
  [count]
);

This count that whenever count changes, a new function with stabilization is returned.

In summary, useMemo is recommended for scenarios that use higher-order functions

Some netizens have provided valuable feedback, I continue to add: just using useMemo, there are still some problems.

Question 1: UseMemo "future" is not "stable"

React's official documentation mentions that you can useMemo as a means of performance optimization, but don't use it as a semantic guarantee. I n the future, React may choose to "forget" some of the previous memoed values and recalculate them the next time they are rendered, such as freeing up memory for off-screen components. W rite code that you can do without > useMemo -- and then add > useMemo to your code to optimize performance. T hat is, in some particular case in the future, this stabilization function will still fail. O f course, this happens "in the future" and is relatively extreme, with a lower probability of occurrence, and even if it does, it will not occur "continuously for a short period of time". So for scenarios that aren't "front-end shaking is going to end", the risk is relatively small.

Problem 2: UseMemo does not solve all higher-order function scenarios once and for all

In the example scenario, the anti-shake logic is: "One second after consecutive clicks, the logic is really executed, and the repeated clicks in the process fail." And if the business logic changes to "a state change occurs immediately after clicking, and repeated clicks are invalid within the next second," then our code may become.

const handleClick = useMemo( 
  () => throttle(() => { setCount(count + 1); }, 1000), [count] );

Then it was found to be dead again. The reason is that count changes immediately after clicking, and then handleClick repeats the new function, and the throttle fails.

So this scenario, the idea has changed back to the aforementioned, "eliminate dependency" or "use ref".

Of course, you can also choose to implement a debounce or throttle manually yourself. I suggest writing two implementations yourself using the community's libraries, such as react-use, or referring to their implementations.

The above is W3Cschool编程狮 about react hooks online bug after the re-release of the relevant introduction, I hope to help you.