SoFunction
Updated on 2025-04-07

React hooks asynchronous operation pitfall record

A summary of some pitfalls you will encounter when using useEffect and asynchronous tasks.

Three common questions:

1. How to initiate asynchronous tasks when component loads

2. How to initiate asynchronous tasks during component interaction

​ 3. Other traps

1. React hooks send asynchronous requests

1. Use useEffect to initiate an asynchronous task. The second parameter uses an empty array to implement the method body when the component is loaded. The return value function is executed once when the component is unloaded to clean up some things.

2. Use AbortController or the semaphore ( ) provided by some libraries to control the abort request and exit more gracefully

3. When you need to set a timer in other places (such as in the click processing function) and clean up in the useEffect return value, use local variables or useRef to record this timer, and do not use useState.

4. When closures such as setTimeout appear in the component, try to refer to ref instead of state inside the closure, otherwise it is easy to read old values.

5. The update status method returned by useState is asynchronous, and a new value must be obtained next time you repaint. Don't try to get the status immediately after changing it.

2. How to initiate asynchronous tasks when component loads

This type of requirement is very common. A typical example is to send a request to the backend when the list component is loaded to get the list display.

import React, { useState, useEffect } from 'react';
  const SOME_API = '/api/get/value';
  export const MyComponent: <{}> = => {
  const [loading, setLoading] = useState(true);
  const [value, setValue] = useState(0);
  useEffect( => {
    (async => {
      const res = await fetch(SOME_API);
      const data = await ;
      setValue();
      setLoading(false);
    });
  }, []);
  return (
    <>
      {
        loading ? (
        <h2>Loading...</h2>
        ) : (
        <h2>value is {value}</h2>
        )
      }
    </>
  );
}

As mentioned above, a basic component with Loading function will send an asynchronous request to the backend to obtain a value and display it on the page.

If it is sufficient by example standards, but it should be applied to the project,I have to consider a few issues

3. What happens if the component is destroyed before the response comes back?

At this time, React will report a ⚠️ warning message:

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 subions and asynchronous tasks in a useEffect cleanup Notification

That is to say, a component should not be modified after it is uninstalled. Although it does not affect operation, this problem should not exist. So how to solve it?

The core of the problem is that after the component is uninstalled, setValue() and setLoading(false) are still called to change the state.

Therefore, an easy way is to mark whether the component has been uninstalled, and you can use the return value of useEffect.

//Omit other contents of the component and list only diffuseEffect( =&gt; {
  let isUnmounted = false;
  (async =&gt; {
    const res = await fetch(SOME_API);
    const data = await ;
    if (!isUnmounted) {
      setValue();
      setLoading(false);
    }
  });
  return =&gt; {
  isUnmounted = true;
  }
}, []);

This will prevent this Warning smoothly.

Is there a more elegant solution?

The above approach is to make a judgment when a response is received, that is, it is a bit passive when the response is completed no matter what.

A more proactive way is to detect that the request is directly interrupted during uninstallation, so naturally there is no need to wait for the response.

This proactive solution requires the use of AbortController.

AbortControllerIt is an experimental interface for a browser, it canReturns a semaphore (singal), thereby aborting the sent request.

This interface has good compatibility, except IE (such as Chrome, Edge, FF and most mobile browsers, including Safari).

useEffect( =&gt; {
  let isUnmounted = false;
  const abortController = new AbortController; // Create  (async =&gt; {
    const res = await fetch(SOME_API, {
    singal: , // As a semaphore    });
    const data = await ;
    if (!isUnmounted) {
      setValue();
      setLoading(false);
    }
  });
  return =&gt; {
    isUnmounted = true;
    ; // Interrupted during component uninstallation  }
}, []);

The implementation of singal relies on the method used to actually send requests, the fetch method as shown in the above example accepts the single attribute.

If you are using axios, it already contains it and can be used directly, an example is here.

import React, { Component } from 'react';
import axios from 'axios';

class Example extends Component {
  signal = ();

  state = {
    isLoading: false,
    user: {},
  }
  
  componentDidMount() {
    ();
  }
  
  componentWillUnmount() {
    ('Api is being canceled');
  }
  
  onLoadUser = async () => {
    try {
      ({ isLoading: true });
      const response = await ('/api/', {
        cancelToken: ,
      })
      ({ user: , isLoading: true });
    } catch (err) {
      if ((err)) {
        ('Error: ', ); // => prints: Api is being canceled
      } else {
        ({ isLoading: false });
      }
    }
   } 
    render() {
      return (
        <div>
          <pre>{(, null, 2)}</pre>
        </div>
      )
    }
 
}

4. How to initiate asynchronous tasks during component interaction

Another common requirement isSend a request or turn on the timer when the component interacts (such as clicking a button), modify the data after receiving the response and then affect the page

The biggest difference here and the above section (when component loading) is that React Hooks can only be written at the component level, not at the method (dealClick) or control logic (if, foretc.) written internally, so you cannot call it in the response function of the clickuseEffect. But we still need to use ituseEffectreturn function to clean up.

​ Take the timer as an example. Suppose we want to make a component, and turn on a timer (5s) after clicking the button, and modify the status after the timer is finished.

But if the component is destroyed before the timer is reached, we want to stop this timer to avoid memory leakage. If implemented in code, you will find that turning on the timer and cleaning the timer will be in different places, so you must record this timer.

See the following example:

import React, { useState, useEffect } from 'react';
 
export const MyComponent: &lt;{}&gt; = () =&gt; {
    const [value, setValue] = useState(0);
 
    let timer: number;
 
    useEffect(() =&gt; {
        // timer needs to be created when clicked, so only cleanup is used here        return () =&gt; {
            ('in useEffect return', timer); // <- Correct value            (timer);
        }
    }, []);
 
    function dealClick() {
        timer = (() =&gt; {
            setValue(100);
        }, 5000);
    }
 
    return (
        &lt;&gt;
            &lt;span&gt;Value is {value}&lt;/span&gt;
            &lt;button onClick={dealClick}&gt;Click Me!&lt;/button&gt;
        &lt;/&gt;
    );
}

Since you want to record the timer, you can naturally use an internal variable to store it (not considering that clicking the button continuously causes multiple times to appear, assuming that you only click once. Because clicking the button in actual situations will trigger other state changes, and then the interface changes, so you can't click it).

Need hereNoticeYes,IftimerUpgrade to state, the code will insteadProblems arise

Consider the following code:

import React, { useState, useEffect } from 'react';
 
export const MyComponent: &lt;{}&gt; = () =&gt; {
    const [value, setValue] = useState(0);
    const [timer, setTimer] = useState(0); // Upgrade timer to state 
    useEffect(() =&gt; {
        // timer needs to be created when clicked, so only cleanup is used here        return () =&gt; {
            ('in useEffect return', timer); // &lt;- 0
            (timer);
        }
    }, []);
 
    function dealClick() {
        let tmp = (() =&gt; {
            setValue(100);
        }, 5000);
        setTimer(tmp);
    }
 
    return (
        &lt;&gt;
            &lt;span&gt;Value is {value}&lt;/span&gt;
            &lt;button onClick={dealClick}&gt;Click Me!&lt;/button&gt;
        &lt;/&gt;
    );
}

SemanticallytimerLet’s put aside whether it is considered a component’s state, just look at the code level.

useuseStateCome and remembertimerStatus, usesetTimerIt seems reasonable to change the status.

But actually running,useEffectThe returned cleanup function is obtainedtimerIt is the initial value, i.e.0

Why are there any differences between the two writing methods?

The core of it lies in whether the variable written and the variable read are the same variable.

The first way to write: The code istimerUsed as a local variable within a component. When the component is rendered for the first time,useEffectThe returned closure function points to this local variabletimer. existdealClickWhen setting the timer, the return value is still written to this local variable (that is, both read and write are the same variable), so during subsequent uninstallation, although the component reruns, a new local variable appearstimer, but this does not affect the old ones in the closuretimer, so the result is correct.

The second way to write: timerIt's oneuseStateThe return value of , is not a simple variable. Judging from the source code of React Hooks, it returns[, dispatch], corresponding to the value we are connected to and change method. When calledsetTimerandsetValueWhen the repaint is triggered twice respectively, so thatPointed tonewState(Note: not modify, but repoint). butuseEffectReturn to the closuretimerStill pointing to the old state, so that the new value cannot be obtained. (That is, the old value is read, but the new value is written, not the same)

​ If you feel it is difficult to read the source code of Hooks, you can understand it from another perspective: Although React launched Hooks in 16.8, it actually only strengthened the writing of functional components to have states and used them as a substitute for class components, but the internal mechanism of React state has not changed. In ReactsetStateInside, after combining the new state and the old state through the merge operation, return to a new state object. No matter how Hooks are written, this principle has not changed. Now the closure points to the old state object,setTimerandsetValueRegenerating and pointing to a new state object does not affect the closure, causing the closure to not read the new state.

We noticed that React also provides us with oneuseRef, its definition is:

  • useRef Returns a mutable ref object withcurrentThe attribute is initialized as an incoming parameter (initialValue).
  • The returned ref objectStay unchanged throughout the life of the component.

The ref object ensures that the value remains unchanged throughout the life cycle and is updated synchronously because the return value of ref always has only one instance, and all reads and writes point to itself. So it can also be used to solve the problem here.

import React, { useState, useEffect, useRef } from 'react';
 
export const MyComponent: &lt;{}&gt; = () =&gt; {
    const [value, setValue] = useState(0);
    const timer = useRef(0);
 
    useEffect(() =&gt; {
        // timer needs to be created when clicked, so only cleanup is used here        return () =&gt; {
            ();
        }
    }, []);
 
    function dealClick() {
         = (() =&gt; {
            setValue(100);
        }, 5000);
    }
 
    return (
        &lt;&gt;
            &lt;span&gt;Value is {value}&lt;/span&gt;
            &lt;button onClick={dealClick}&gt;Click Me!&lt;/button&gt;
        &lt;/&gt;
    );
}

In fact, we will see later.useRefIt is safer and safer to cooperate with asynchronous tasks.

V. Other traps

Modification status is asynchronous

//Error Exampleimport React, { useState } from 'react';
 
export const MyComponent: &lt;{}&gt; = () =&gt; {
    const [value, setValue] = useState(0);
 
    function dealClick() {
        setValue(100);
        (value); // &lt;- 0
    }
 
    return (
        &lt;span&gt;Value is {value}, AnotherValue is {anotherValue}&lt;/span&gt;
    );
}

useStateThe returned modification function is asynchronous and will not take effect directly after being called, so it is read immediately.valueWhat is obtained is the old value (0)。

The purpose of React is designed for performance considerations. It is easy to understand that all states are changed and redrawn only once can solve the update problem, rather than changing and redrawing once.

No new value for other states can be read in timeout

//Error Exampleimport React, { useState, useEffect } from 'react';
 
export const MyComponent: &lt;{}&gt; = () =&gt; {
    const [value, setValue] = useState(0);
    const [anotherValue, setAnotherValue] = useState(0);
 
    useEffect(() =&gt; {
        (() =&gt; {
            ('setAnotherValue', value) // &lt;- 0
            setAnotherValue(value);
        }, 1000);
        setValue(100);
    }, []);
 
    return (
        &lt;span&gt;Value is {value}, AnotherValue is {anotherValue}&lt;/span&gt;
    );
}

This question is used aboveuseStateGo to recordtimerSimilarly, when generating a timeout closure, the value of value is 0.

Although afterwardsetValueThe state has been modified, but React has pointed to the new variable inside, and the old variable is still referenced by the closure, so the closure still gets the old initial value, that is, 0.

To fix this problem, it is still useduseRef,as follows:

import React, { useState, useEffect, useRef } from 'react';
 
export const MyComponent: <{}> = () => {
    const [value, setValue] = useState(0);
    const [anotherValue, setAnotherValue] = useState(0);
    const valueRef = useRef(value);
     = value;
 
    useEffect(() => {
        (() => {
            ('setAnotherValue', ) // <- 100
            setAnotherValue();
        }, 1000);
        setValue(100);
    }, []);
 
    return (
        <span>Value is {value}, AnotherValue is {anotherValue}</span>
    );
}

Or timeout problem

Suppose we want to implement a button, which displays false by default. Change to true after clicking, but then return to false after two seconds (true and false are interchangeable).

Consider the following code:

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

We will find that we can switch normally when clicking, but it will not change back in two seconds.

The reason is stilluseStateThe update is to repoint the new value, but the closure of timeout still points to the old value.

So in the example,flagAlwaysfalse, although subsequentsetFlag(!flag), but it still does not affect the timeoutflag

There are two solutions.

The first one is to use ituseRef

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

The second is to usesetFlagCan accept functions as parameters and use closures and parameters to implement (functional update)

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

whensetFlagWhen the parameter is a function type, the meaning of this function is to tell React how toCurrent statusGenerateA new state(Similar to the reducer of redux, but only for one state child reducer). Since it is the current state, the effect can be achieved by inversely replacing the return value.

Summarize

​ When asynchronous tasks appear in Hook, especially timeout, we must pay special attention.

useStateOnly guarantee the state between multiple repaintsvalueIt's the same, but it's not guaranteed that they are the same object.Therefore, when a closure reference occurs, try to use it as much as possibleuseRefInstead of using the state itself directly, otherwise it will be easy to get stuck.

On the contrary, if you do encounter a situation where a new value is set but the old value is read, you can also think about it in this direction, which may be due to this reason.

The above is personal experience. I hope you can give you a reference and I hope you can support me more.