与 useState 的异步性质相比,React 的 useReducer 是同步的吗?
Is React's useReducer is synchronous compared to asynchronous nature of useState?
我的印象是 useState
和 useReducer
的工作方式相似,除了当状态为 complex/nested 个对象时我们应该使用 useReducer。
但是今天我发现了一个奇怪的行为,我正在遍历一个数组并将值设置为一个状态对象。我使用 useState
和 useReducer
.
做了同样的例子
With useState
: 它只会将数组中的最后一个值推送到状态对象,因为 useState 本质上是异步的,所以当我们在循环中设置状态时,它可能无法根据之前的状态正确更新.所以你只得到状态中的最后一个对象。
With useReducer
:我期待与 useReducer 相同的行为,但是对于 useReducer,当我们从循环内部执行 dispatch
操作时,它似乎正确设置了状态。所以在这里你得到了状态内的所有对象。
useState
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>
);
}
使用Reducer
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>
);
}
I was in a impression that both useState and useReducer works similarly
这个印象是正确的。如果我记得,useState
甚至 调用 useReducer
,所以 useState
基本上是 useReducer
的特例(我查了一下之前在 React 的源代码中,但我再也找不到它来提供 link).
您看到的行为与它是同步还是异步无关,而是关于您是根据闭包中的值计算新状态,还是根据最新值计算新状态.考虑这段代码:
const [students, setStudents] = React.useState({});
//... some code omitted
setStudents({...students, ...student})
注意学生是const
。它永远不会改变,即使您调用 setStudents
也不会改变。如果您只设置状态一次,那不是真正的问题:您将复制一份学生,添加一个新学生,然后将其传递进去。然后该组件会重新呈现。在那个新的渲染器上,将创建一个新的局部变量,其中包含一个学生的新对象。然后,新渲染中的代码可以与一名学生与该对象进行交互,并可能与两名学生一起创建一个对象。
但是,如果您在循环中执行此操作,则每次循环都从空对象开始。您复制空对象,添加学生 A,并告诉 React 将状态设置为该对象。 students
没有改变,所以你再次复制同一个空对象,并添加学生 B。请注意,这第二次不包括 A。最终循环结束,唯一重要的是 setStudents最后一个。
设置状态时,可以使用另一种形式。您传入一个函数,React 将使用最新值调用该函数:
setStudents(previous => {
return {
...previous,
...student
}
});
使用这种方法,previous 以空对象开始,然后添加学生 A。然后在下一次循环中,previous 现在是包含学生 A 的对象,然后添加学生 B。所以当你完成后,你将拥有所有学生,而不仅仅是最后一个。
回到你关于 useState 和 useReducer 的问题:它们不同的原因是 useReducer 总是使用回调形式。 reducer 函数将始终以最新状态传递,因此您是基于该状态计算新状态,而不是基于任何 students
等于组件上次呈现的时间。如果您重新编写代码以使用如上所示的回调,您也可以在 useState 中使用同样的东西。
我的印象是 useState
和 useReducer
的工作方式相似,除了当状态为 complex/nested 个对象时我们应该使用 useReducer。
但是今天我发现了一个奇怪的行为,我正在遍历一个数组并将值设置为一个状态对象。我使用 useState
和 useReducer
.
With useState
: 它只会将数组中的最后一个值推送到状态对象,因为 useState 本质上是异步的,所以当我们在循环中设置状态时,它可能无法根据之前的状态正确更新.所以你只得到状态中的最后一个对象。
With useReducer
:我期待与 useReducer 相同的行为,但是对于 useReducer,当我们从循环内部执行 dispatch
操作时,它似乎正确设置了状态。所以在这里你得到了状态内的所有对象。
useState
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>
);
}
使用Reducer
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>
);
}
I was in a impression that both useState and useReducer works similarly
这个印象是正确的。如果我记得,useState
甚至 调用 useReducer
,所以 useState
基本上是 useReducer
的特例(我查了一下之前在 React 的源代码中,但我再也找不到它来提供 link).
您看到的行为与它是同步还是异步无关,而是关于您是根据闭包中的值计算新状态,还是根据最新值计算新状态.考虑这段代码:
const [students, setStudents] = React.useState({});
//... some code omitted
setStudents({...students, ...student})
注意学生是const
。它永远不会改变,即使您调用 setStudents
也不会改变。如果您只设置状态一次,那不是真正的问题:您将复制一份学生,添加一个新学生,然后将其传递进去。然后该组件会重新呈现。在那个新的渲染器上,将创建一个新的局部变量,其中包含一个学生的新对象。然后,新渲染中的代码可以与一名学生与该对象进行交互,并可能与两名学生一起创建一个对象。
但是,如果您在循环中执行此操作,则每次循环都从空对象开始。您复制空对象,添加学生 A,并告诉 React 将状态设置为该对象。 students
没有改变,所以你再次复制同一个空对象,并添加学生 B。请注意,这第二次不包括 A。最终循环结束,唯一重要的是 setStudents最后一个。
设置状态时,可以使用另一种形式。您传入一个函数,React 将使用最新值调用该函数:
setStudents(previous => {
return {
...previous,
...student
}
});
使用这种方法,previous 以空对象开始,然后添加学生 A。然后在下一次循环中,previous 现在是包含学生 A 的对象,然后添加学生 B。所以当你完成后,你将拥有所有学生,而不仅仅是最后一个。
回到你关于 useState 和 useReducer 的问题:它们不同的原因是 useReducer 总是使用回调形式。 reducer 函数将始终以最新状态传递,因此您是基于该状态计算新状态,而不是基于任何 students
等于组件上次呈现的时间。如果您重新编写代码以使用如上所示的回调,您也可以在 useState 中使用同样的东西。