与 useState 的异步性质相比,React 的 useReducer 是同步的吗?

Dee*_*dhy 3 javascript reactjs react-hooks use-reducer

我的印象是useStateuseReducer除了当状态是复杂/嵌套对象时我们应该使用 useReducer 之外,两者的工作方式相似。

但是今天我发现了一个奇怪的行为,我正在遍历一个数组并将值设置为一个状态对象。我使用useState和做了同样的例子useReducer

With useState:它只将数组中的最后一个值推送到状态对象,因为 useState 本质上是异步的,所以当我们在循环中设置状态时,它可能无法根据之前的状态正确更新。所以你只得到状态中的最后一个对象。

使用useReducer:我期待 useReducer 具有相同的行为,但是使用 useReducer,当我们dispatch从循环内部执行操作时,它似乎可以正确设置状态。所以在这里你得到状态内的所有对象。

使用状态

import React from 'react';
import ReactDOM from 'react-dom';

function App() {
  const [students, setStudents] = React.useState({});
  
  const createStudents = () => {
    const ids = [1,2,3];
    const names = ['john', 'michael', 'greg']
    for(let i = 0; i < 3; i++){
      const student = {[ids[i]]: names[i]};
      setStudents({...students, ...student})
    }
  }
  return (
    <div className="App">
      <button onClick={createStudents}>Create Students</button>
      <br />
      {JSON.stringify(students)}
    </div>
  );
}

   
Run Code Online (Sandbox Code Playgroud)

使用减速器

import React from 'react';
import ReactDOM from 'react-dom';

function App() {
  const studentReducer = (state, action) => {
    switch (action.type) {
      case 'ADD':
        return {...state, students: {...state.students, ...action.payload}};
      default:
        throw new Error();
    }
  }

  const [students, dispatch] = React.useReducer(studentReducer, {students: {}});

  const createStudents = () => {
    const ids = [1,2,3];
    const names = ['john', 'michael', 'greg']
    for(let i = 0; i < 3; i++){
      const student = {[ids[i]]: names[i]};
      dispatch({type: 'ADD', payload: student})
    }
  }
  return (
    <div className="App">
      <button onClick={createStudents}>Create Students</button>
      <br />
      {JSON.stringify(students)}
    </div>
  );
}
Run Code Online (Sandbox Code Playgroud)

Nic*_*wer 6

我的印象是 useState 和 useReducer 的工作方式相似

这种印象是正确的。如果我记得,useState甚至调用 useReducer,所以实际上useState基本上是一个特例useReducer(我之前在 react 的源代码中查找过这个,但我找不到它来提供链接了)。

您所看到的行为与它是同步还是异步无关,而是关于您是根据闭包中的值计算新状态,还是根据最新值计算新状态。考虑这个代码:

const [students, setStudents] = React.useState({});
//... some code omitted
setStudents({...students, ...student})
Run Code Online (Sandbox Code Playgroud)

请注意,学生是一个const. 它永远不会改变,即使你调用setStudents. 如果您只设置一次状态,那不是真正的问题:您将制作学生的副本,添加一个新学生,然后将其传入。然后组件会重新渲染。在那个新的渲染中,将创建一个新的局部变量,其中包含一个学生的新对象。然后,该新渲染中的代码可以与一名学生与该对象进行交互,并可能与两名学生一起创建一个。

但是如果你在一个循环中进行,那么每次循环你都是从空对象开始的。您复制空对象,添加学生 A,并告诉 react 将状态设置为该对象。students没有改变,所以你再次复制相同的空对象,并添加学生 B。注意第二次不包括 A。最终循环完成,唯一重要的 setStudents 是最后一个。

设置状态时,您可以使用另一种形式。您传入一个函数,react 将使用最新的值调用该函数:

setStudents(previous => {
  return { 
    ...previous, 
    ...student 
  }
});
Run Code Online (Sandbox Code Playgroud)

使用这种方法,前一个作为空对象开始,你添加学生 A。然后在下一次循环中,前一个现在是包含学生 A 的对象,你添加学生 B。所以当你完成后,您将拥有所有学生,而不仅仅是最后一个。


所以回到你关于 useState 和 useReducer 的问题:它们不同的原因是 useReducer 总是使用回调形式。reducer 函数将始终以最近的状态传递,因此您将基于该状态来计算新状态,而不是基于students上次渲染组件的时间。如果您重做代码以使用如上所示的回调,您也可以在 useState 中获得相同的功能。