React Hooks are a new feature in React 16.8 that allows functional components to change from stateless to stateful using functions such as states without writing classes. React.SFC (Stateless Functional Component) is also changed to react. FC (Functional Component) in @types/ React.

With this upgrade, components written in the original class can be completely replaced by functional components. While the decision to convert all class components from an old project to functional components will vary from person to person, the new component is worth the effort because it does reduce the amount of code, Especially for repetitive code (e.g. ComponentDidMount + componentDidUpdate + componentWillUnmount = useEffect).

It has been more than half a year since the release of 16.8 (February this year), but MY skills are limited, especially when using the useEffect with asynchronous tasks. Special for this article, as a record, for the same problem encountered colleagues reference. I’m going to talk about three very common problems in projects:

  1. How do I initiate an asynchronous task while a component is loading
  2. How do I initiate asynchronous tasks when components interact
  3. Other trap

TL; DR

  1. useuseEffectThe second argument uses an empty array to execute the method body when the component is loaded. The return value function executes once when the component is unloaded to clean up something, such as a timer.
  2. Use AbortController or the semaphore that comes with some libraries (axios.CancelToken) to control abort requests and exit more gracefully.
  3. When you need to set a timer somewhere else (such as in the click-handler function), set the timer inuseEffectFor cleanup in return values, use local variables oruseRefTo record thistimer.Don’tuseuseState.
  4. Appear in componentsetTimeoutWhen waiting for a closure, try to refer to ref inside the closure instead of state, otherwise it is easy to read old values.
  5. useStateThe status update method returned is asynchronous and will not get the new value until the next redraw. Do not try to get the state immediately after changing it.

How do I initiate an asynchronous task while a component is loading

This type of requirement is very common. A typical example is to send a request to the back end when a list component is loaded, and then fetch the list and present it.

Sending requests is also one of the side effects defined by React, so you should write it using useEffect. I won’t elaborate on the basic syntax, but the code looks like this:

import React, { useState, useEffect } from 'react';

const SOME_API = '/api/get/value';

export const MyComponent: React.FC<{}> = (a)= > {
    const [loading, setLoading] = useState(true);
    const [value, setValue] = useState(0);

    useEffect((a)= > {
        (async () = > {const res = await fetch(SOME_API);
            const data = await res.json();
            setValue(data.value);
            setLoading(false);
        })(a); } []);return (
        <>
        {loading ? (

Loading...

) : ( <h2>value is {value}</h2> )} < / a >
); }
Copy the code

This is a basic Loading component that sends an asynchronous request to the back end to get a value and display it on the page. If this is sufficient by the standards of the example, you have to consider several issues before you can actually apply them to a project.

What if the component is destroyed before the response comes back?

React will report a Warning

Warning: Can’t perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.in Notification

The state of a component should not be modified after it has been uninstalled. Although it does not affect the operation, but as a representative of the perfectionist programmer group can not tolerate this situation, so how to solve it?

The core of the problem is that setValue(data.value) and setLoading(false) are called to change the state even after the component is unloaded. An easy way to do this is to flag if the component has been unloaded, using the return value of useEffect.

// Omit the rest of the component and list only the diff
useEffect((a)= > {
    let isUnmounted = false;
    (async () = > {const res = await fetch(SOME_API);
        const data = await res.json();
        if (! isUnmounted) {
            setValue(data.value);
            setLoading(false); }})(a);

    return (a)= > {
        isUnmounted = true; }} []);Copy the code

This will avoid this Warning.

Is there a more elegant way to do it?

The above approach is to judge the response as it is received, that is, wait for the response to complete anyway, which is a bit passive. A more proactive approach is to detect the interruption of the request at uninstall time without waiting for a response. This active scheme requires AbortController.

AbortController is an experimental interface for the browser that returns a semaphore (Singal) to abort the sent request. The interface is compatible with all but Internet Explorer (Chrome, Edge, FF, and most mobile browsers, including Safari).

useEffect((a)= > {
    let isUnmounted = false;
    const abortController = new AbortController(); / / create
    (async() = > {const res = await fetch(SOME_API, {
            singal: abortController.singal, // passed in as a semaphore
        });
        const data = await res.json();
        if(! isUnmounted) { setValue(data.value); setLoading(false);
        }
    })();

    return (a)= > {
        isUnmounted = true;
        abortController.abort(); // Interrupts during component uninstallation}} []);Copy the code

The implementation of Singal depends on the method used to actually send the request, as the fetch method in the above example accepts the Singal attribute. If you are using axios, it already contains axios.cancelToken internally and can be used directly, as shown here.

How do I initiate asynchronous tasks when components interact

Another common requirement is to send a request or start a timer when a component interacts, such as clicking a button, and then modify the data to affect the page when the response is received. The biggest difference between this and the previous section (component loading) is that the React Hooks can only be written at the component level, not inside methods (dealClick) or control logic (if, for, etc.), so useEffect cannot be called from the click response function. However, we still use the useEffect return function to do the cleanup.

Take a timer as an example. Suppose we want to make a component that starts a timer (5s) when the button is clicked, and changes the state when the timer ends. But if we destroy the component before the timer runs out, we want to stop the timer to avoid memory leaks. In code, you’ll see that the start timer and the clean timer are in different places, so you’ll have to log the timer. Look at the following example:

import React, { useState, useEffect } from 'react'; export const MyComponent: React.FC<{}> = () => { const [value, setValue] = useState(0); let timer: number; UseEffect (() => {// timer needs to be set up when clicked, so only clean up using return () => {console.log('in useEffect return', timer); // <- The correct value window.clearTimeout(timer); }} []); function dealClick() { timer = window.setTimeout(() => { setValue(100); }, 5000); } return ( <> <span>Value is {value}</span> <button onClick={dealClick}>Click Me! </button> </> ); }Copy the code

Since the timer is to be recorded, it is natural to use an internal variable to store it. In fact, clicking the button will also trigger other state changes, and then the interface changes, so it can not be clicked).

The important thing to note here is that if you upgrade the timer to state, the code will actually break. Consider the following code:

import React, { useState, useEffect } from 'react'; export const MyComponent: React.FC<{}> = () => { const [value, setValue] = useState(0); const [timer, setTimer] = useState(0); UseEffect (() => {// The timer needs to be set when clicked, Return () => {console.log('in useEffect return', timer); // <- 0 window.clearTimeout(timer); }} []); function dealClick() { let tmp = window.setTimeout(() => { setValue(100); }, 5000); setTimer(tmp); } return ( <> <span>Value is {value}</span> <button onClick={dealClick}>Click Me! </button> </> ); }Copy the code

Leaving aside the question of whether a Timer counts semantically as a component state, let’s just look at the code level. It seems reasonable to use useState to remember the timer state and setTimer to change the state. However, after the actual operation, the timer in the cleaning function returned by useEffect is the initial value, that is, 0.

Why is there a difference between the two?

The core of this is whether the variable being written and the variable being read are the same variable.

The first way to write the code is to use the timer as a local variable within the component. This local timer is referred to in the closure function returned by useEffect when the component is first rendered. When the timer is set in dealClick, the return value is still written to the local variable (that is, both read and write are the same variable), so on subsequent uninstallation, the re-run of the component causes a new local variable timer to appear, but does not affect the old timer inside the closure, so the result is correct.

Second, timer is a return value of useState, not a simple variable. From the React Hooks source, it returns hook. MemorizedState, dispatch, corresponding to the values and change methods we attached. When setTimer and setValue are called, two redraws are triggered, respectively, so that hook. MemorizedState points to newState (note: not modified, but repointed). UseEffect returns that the timer in the closure still points to the old state and therefore does not get the new value. (read the old value, but write the new value, not the same value)

If you have trouble reading the Hooks source code, there is another way to understand it: While React introduced Hooks in 16.8, it actually only strengthens the way functional components are written to have state as an alternative to class components, the internal mechanics of React state remain unchanged. In React, setState merges the old state with the new state and returns a new state object. This principle remains unchanged, regardless of what Hooks write. Now the closure points to the old state object, and setTimer and setValue are regenerated to point to the new state object. This does not affect the closure, so the closure cannot read the new state.

We notice React also provides us with a useRef, which is defined as

UseRef returns a mutable REF object whose current property is initialized as the passed parameter (initialValue). The ref object returned remains constant throughout the life of the component.

The ref object is guaranteed to be constant and updated synchronously throughout its life because the return value of ref is always a single instance, with all reads and writes pointing to itself. So it can also be used to solve the problem here.

import React, { useState, useEffect, useRef } from 'react'; export const MyComponent: React.FC<{}> = () => { const [value, setValue] = useState(0); const timer = useRef(0); UseEffect (() => {// timer needs to be set up when clicked, so just use return () => {window.clearTimeout(timer.current); }} []); function dealClick() { timer.current = window.setTimeout(() => { setValue(100); }, 5000); } return ( <> <span>Value is {value}</span> <button onClick={dealClick}>Click Me! </button> </> ); }Copy the code

In fact, as we’ll see later, useRef is more secure when paired with asynchronous tasks.

Other trap

The change state is asynchronous

This is actually pretty basic.

import React, { useState } from 'react';

export const MyComponent: React.FC<{}> = () => {
    const [value, setValue] = useState(0);

    function dealClick() {
        setValue(100);
        console.log(value); // <- 0
    }

    return (
        <span>Value is {value}, AnotherValue is {anotherValue}</span>
    );
}
Copy the code

The modification function returned by useState is asynchronous and does not take effect immediately, so the old value (0) is immediately read.

React is designed for performance purposes. It is easy to understand how to change all states and only redraw them once.

No new values for other states can be read in timeout

import React, { useState, useEffect } from 'react'; export const MyComponent: React.FC<{}> = () => { const [value, setValue] = useState(0); const [anotherValue, setAnotherValue] = useState(0); useEffect(() => { window.setTimeout(() => { console.log('setAnotherValue', value) // <- 0 setAnotherValue(value); }, 1000); setValue(100); } []); return ( <span>Value is {value}, AnotherValue is {anotherValue}</span> ); }Copy the code

The problem is similar to using useState to log the timer above, where the value is 0 when the timeout closure is generated. Although the state was later changed with setValue, React was already pointing to new variables inside, while the old variables were still referenced by the closure, so the closure still got the old initial value, which was 0.

To fix this, use useRef again, as follows:

import React, { useState, useEffect, useRef } from 'react'; export const MyComponent: React.FC<{}> = () => { const [value, setValue] = useState(0); const [anotherValue, setAnotherValue] = useState(0); const valueRef = useRef(value); valueRef.current = value; useEffect(() => { window.setTimeout(() => { console.log('setAnotherValue', valueRef.current) // <- 100 setAnotherValue(valueRef.current); }, 1000); setValue(100); } []); return ( <span>Value is {value}, AnotherValue is {anotherValue}</span> ); }Copy the code

Again, timeout

Suppose we want to implement a button that displays false by default. Changed to True when clicked, but returned to false after two seconds (true and false are interchangeable). Consider the following code:

import React, { useState } from 'react'; export const MyComponent: React.FC<{}> = () => { const [flag, setFlag] = useState(false); function dealClick() { setFlag(! flag); setTimeout(() => { setFlag(! flag); }, 2000); } return ( <button onClick={dealClick}>{flag ? "true" : "false"}</button> ); }Copy the code

We’ll see that the switch works fine when clicked, but it doesn’t change back after two seconds. The reason is that the useState update repoints to the new value, but the timeout closure still points to the old value. So in this example, flag is always false, although subsequent setFlag(! Flag), but still does not affect the flag inside timeout.

There are two solutions.

The first one is again using useRef

import React, { useState, useRef } from 'react'; export const MyComponent: React.FC<{}> = () => { const [flag, setFlag] = useState(false); const flagRef = useRef(flag); flagRef.current = flag; function dealClick() { setFlag(! flagRef.current); setTimeout(() => { setFlag(! flagRef.current); }, 2000); } return ( <button onClick={dealClick}>{flag ? "true" : "false"}</button> ); }Copy the code

The second is to use setFlag to accept functions as arguments and use closures and arguments to do so

import React, { useState } from 'react'; export const MyComponent: React.FC<{}> = () => { const [flag, setFlag] = useState(false); function dealClick() { setFlag(! flag); setTimeout(() => { setFlag(flag => ! flag); }, 2000); } return ( <button onClick={dealClick}>{flag ? "true" : "false"}</button> ); }Copy the code

When setFlag is a function, it tells React how to create a new reducer from the current state (similar to redux’s reducer, but with a single state). Since it is the current state, the return value is reversed, and the effect is achieved.

conclusion

Watch out for asynchronous tasks in a Hook, especially when timeout occurs. UseState can only guarantee that the state values will be the same between multiple redraws, but not that they will be the same object, so use useRef instead of state for closure references. Conversely, if you do get a new value and you get an old value, you can also think in this direction, because that might be the reason.

Refer to the article

  • UseRef on the official website
  • How to create React custom hooks for data fetching with useEffect
  • setTimeout in React Components Using Hooks
  • React – useState – why setTimeout function does not have latest state value?