关于setState使用的一些坑

setState更新数组

你会发现,如果直接使用push等方法改变state,按理来说,push会改变原数组,数组应该更新,但渲染出来的state并不会更改

let newValue = 1;
const [array, setArray] = useState([]);
const handleChange = (newValue: number) =>{
	array.push(newValue);
	setState(array);//array更新了,但无法触发渲染
	console.log(array);//[1]
	//array增加了newValue,但渲染并未发生改变
}

render:
<p>This array is {JSON.stringify(array)}</p> //[]

这是由于js中,数组的赋值是引用传递的,array.push相当于直接更改了数组对应的内存块,但react内部用于对比的array的内存并没有更改,是指向同一个内存的,setState只做shallow compare,因此没有触发re-render。
可以使用扩展运算符,创建一个新数组,更改内存引用

const handleChange = (newValue: number) =>{
	const newArray = [...array, newValue];
	setState(newArray);//此处本质上是改变了引用
	console.log(array);//[]
	//array并未改变,但渲染改变了
}

render:
<p>This array is {JSON.stringify(array)}</p> //[1]

或者触发展示组件的re-render,这样即使不改变数组的引用,依然可以正确显示变动。

const handleChange = (newValue: number) =>{
	setValue(newValue);
	setState(array.push(newValue));//其他更新触发了组件的re-render,此时可以正常显示变动
	console.log(array);//[1]
	//array改变,且渲染改变
}

render:
<p>This array is {JSON.stringify(array)}</p> //[1]

再给一个直观的例子(感谢我的同事@ling)
直接尝试:https://codepen.io/ling-cao/pen/NWrMRrq

const { useRef, useEffect, useState } = React

const useMemoryState = (init) => {
  const [arr, setArr] = useState(init)
  const lastArrRef = useRef(null)
  const updateArr = next => {
    lastArrRef.current = [...arr];
    console.log(next);
    setArr(next)
  }
  return [arr, updateArr, lastArrRef.current]
}

let i = 0;
const App = () => {
  const [arr, setArr, lastArr] = useMemoryState([0])
  const [updateSign, setUpdateSign] = useState(false)
  
  return(
    <>
      <div className="text"><label>Current array :</label> {JSON.stringify(arr)}</div>
      <div className="box-container">
        <div className="box">
          <h1>Push a number to array</h1>
          <pre>setArr(arr.push(i) && arr)</pre>
          <br />
          <button
            onClick={() => {
              i++;
              setArr(arr.push(i) && arr)
            }}
            className="btn btn-2 btn-2c">
              Try it
           </button>
        </div>
        <div className="box">
          <h1>Push a number to array and renew array</h1> 
          <pre>setArr(arr.push(i) && [...arr])</pre>
          <br />
          <button
            onClick={() => {
              i++;
              setArr(arr.push(i) && [...arr])
            }}
            className="btn btn-2 btn-2c">
              Try it
           </button>
        </div>
        <div className="box">
          <h1>Push a number to array and update another state</h1>
          <pre>setArr(arr.push(i) && arr); setUpdateSign(x => !x)</pre>
          <br />
          <button
            onClick={() => {
              i++;
              {
                setArr(arr.push(i) && arr)
                setUpdateSign(x => !x)
              }
            }}
            className="btn btn-2 btn-2c">
              Try it
           </button>
        </div>
      </div>
      </>
  );
}

逐次点击第二个按钮或第三个按钮都可以正常更新渲染。

点击第一个按钮,通过console可以看出来,array数组值有更新,但没有渲染(Current array 没变);再点其他两个按钮时,会把第一个按钮点击更新的结果一起渲染出来。

侧面展示并不是没有更新数组,而是更新后未渲染。

setState不会立即改变数据

setState某种意义上是类似于异步函数的。

// name is ""
this.setState({
    name: "name"
})
console.log(`name is ${this.state.name}`)

这样写,name是不能正常显示。
最常用的办法就是使用回调函数

this.setState({
    name: "name"
}, () => {
  console.log(`name is ${this.state.name}`)
})

多个setState的更新

setState的“异步”是本身执行的过程和代码是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没办法立马拿到更新后的值,形成了所谓的异步。批量更新优化也是建立在“异步”之上的,如果对同一个值进行多次setState,setState的批量更新策略会对其进行覆盖,取最后一次执行;如果是同时setState多个不同的值,在更新时会对其合并批量更新。

setState异步回调获取不到最新值

  useEffect(() => {
    const newModel = {
      name: props.name,
      datasetId: props.datasetId,
      modelId: null,
      trainingStatus: TrainingStatus.Init,
      modelStatus: Status.NotStarted,
    } as TrainingModel;
    setModels([...models, newModel]);
    startTraining(newModel);
  }, [props.datasetId]);

  const startTraining = async (newModel: TrainingModel) => {
    const dataset = await getDataset(newModel.datasetId);
    let newModels = [...models];
    let currModel = newModels.find(x => x.datasetId == newModels.datasetId);
    currModel.trainingStatus = TrainingStatus.CreateDataset;
    //此时可通过页面的渲染效果知道models中已有值,但此处断点models为空
    setModels(newModels);
  };

类似的,老生常谈的,在useEffect里面设置一个Interval,过了Interval time,也同样是useEffect更新时的state值,而得不到最新的state值。
为解决异步导致的获取不到最新state的问题,使用setState的回调函数获取state的当前最新值

  const startTraining = async (newModel: TrainingModel) => {
    const dataset = await getDataset(newModel.datasetId);
      setModels(lastModels => { //此时的lastModels是models的最新值
        const nextModels = [...lastModels];
        let currModel = nextModels.find(x => x.datasetId == newModel.datasetId);
        currModel.trainingStatus = TrainingStatus.CreateDataset;
        return nextModels;
      });
  };

原因是,组件内部的任何函数,包括事件处理函数和effect,都是从它被创建的那次渲染中被[看到]的,也就是说,组件内部的函数拿到的总是定义它的那次渲染中的props和state。想要解决,一般两种方法,一种是上述的使用setState回调函数获取state最新值,一种是使用ref保存修改并读取state。

posted @ 2020-12-20 18:54  Shaw_喆宇  阅读(2150)  评论(0编辑  收藏  举报