First of all, setState has two ways of passing parameters:
  1. Pass the new value directly: setState(options)
const [state, setState] = useState(0);
setState(state + 1); 
  1. Pass in the callback function: setState(callBack)
const [state, setState] = useState(0);
setState((prevState) => prevState + 1); 
// prevState is the state value before the change, 
// the value returned by return statement will overwrite 
// the state value as the new state. 

Normally setState is passed directly in the first way above. However, there are a few special cases where the first method can be an exception.
For example, you want to get the latest state and set the state in an asynchronous callback or closure. In this case, the state obtained in the first way is not real-time. The official React documentation states that any function inside a component, including event handlers and effects, is “seen” from the rendering in which it was created. So the referenced value is still old, and eventually the setState exception occurs.

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

const App = () => {
  const [arr, setArr] = useState([0]);

  useEffect(() => {
    console.log(arr);
  }, [arr]);

  const handleClick = () => {
    Promise.resolve().then(() => {
      setArr([...arr, 1]); // 此时赋值前 arr 为:[0]
    })
      .then(() => {
        setArr([...arr, 2]); // 此时赋值前 arr 为旧状态仍然为:[0]
      });
  }

  return (
    <>
      <button onClick={handleClick}>change</button>
    </>
  );
}

export default App; 

In the above code, the App component is actually a closure function. handleClick references arr. The value of arr is indeed updated after the first setArr, as we can see in the following screenshot. But the handleClick event handler is still in the old scope, and the arr referenced in it is still old, resulting in [0, 2] after the second setArr.

 

Solution 1 (recommended):

Pass the above code using the second (callback) method

const handleClick = () => {
    Promise.resolve().then(() => {
      setArr(prevState => [...prevState, 1]);
    })
      .then(() => {
        setArr(prevState => [...prevState, 2]);
      });
  } 

Solution 2:

Use forceUpdate in the useReducer mimic component to force the component to render.

Note: This solution is only applicable if the page only relies on this data. If there is a hook like useEffect that listens on this data (arr in the example), it will not be able to capture the changes in real time.

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

const App = () => {
  const [arr, setArr] = useState([0]);
  const [, forceUpdate] = useReducer(x => x + 1, 0);

  const handleClick = () => {
    Promise.resolve().then(() => {
      arr.push(1); // 如果这里也需要做一次渲染在改变状态后调用 forceUpdate() 即可
    })
      .then(() => {
        arr.push(2);
        forceUpdate();
      });
  }

  return (
    <>
      <h1>{arr.toString()}</h1>
      <button onClick={handleClick}>change</button>
    </>
  );
}

export default App; 

Solution 3:

Use ref:

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

const App = () => {
  const [arr, setArr] = useState([0]);
  let ref = useRef();
  useEffect(() => {
    ref.current = arr;
    console.log(arr);
  });

  const handleClick = () => {
    Promise.resolve().then(() => {
      const now = [...ref.current, 1];
      ref.current = now;
      setArr(now);
    })
      .then(() => {
        setArr([...ref.current, 2]);
      });
  }

  return (
    <>
      <h1>{arr.toString()}</h1>
      <button onClick={handleClick}>change</button>
    </>
  );
}

export default App; 

0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *

Catalogue