在存在异步代码的情况下进行多状态更新 (useState)

React multiple state updates (useState) in the presence of async code

提问人:ramcrys 提问时间:10/31/2023 最后编辑:ramcrys 更新时间:10/31/2023 访问量:43

问:

我有以下 React 18(带有 TypeScript)代码:

interface User {
  id: number;
  name: string;
}

function App() {
  const [users, setUsers] = useState<User[]>([]);
  const [error, setError] = useState("");

  useEffect(() => {
    axios
      .get<User[]>("https://jsonplaceholder.typicode.com/users")
      .then((res) => setUsers(res.data));
  }, []);

  const addUser = () => {
    const newUser = { id: 0, name: "Test" };
    setUsers([newUser, ...users]);
    axios
      .post("https://jsonplaceholder.typicode.com/users", newUser)
      .then((res) => {
        console.log("size", users.length);
        setUsers([res.data, ...users]);
      })
      .catch((err) => setError(err.message));
  };

  return (
    <>
      {error && <p>{error}</p>}
      <button onClick={addUser}>Add</button>
      <ul>
        {users.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </>
  );
}

export default App;

据我了解,我们将在这里进行 2 次重新渲染。 第一个将呈现新列表,并在开头附加 id = 0 的新测试用户。Users

然后,在 promise 解析后,第二个将再次呈现最新列表,并在开头附加 id = 11 的(从服务器返回)测试用户。我知道 React 的更新批处理,但这些显然应该是 2 个“批处理”,因此是 2 个重新渲染,对吧?Users

因此,结果应该是与新用户重复的错误列表。

但是,我测试了此代码,它仅使用一个新用户正确呈现列表。我在这里理解错了什么?提前致谢。

编辑:当然,我不是在谈论多次按下按钮。我们应该只讨论只按一次按钮。

IMO -> 即使在第一次按下时,结果也应该重复

结果 -> 不是那样的。这就是为什么我真的很困惑。

javascript reactjs 异步 react-hooks

评论


答:

2赞 David 10/31/2023 #1

因此,结果应该是与新用户重复的错误列表。

继续按下按钮,就会发生这种情况。但它不会在你第一次按下按钮时发生。

为什么?

此操作根据上一次渲染期间捕获的当前版本更新状态:users

setUsers([newUser, ...users]);

然后调用 AJAX 操作,该操作具有更新状态的回调。该回调还会根据上一次渲染期间捕获的当前版本更新状态,因为调用时状态未更新users

setUsers([res.data, ...users]);

这两种情况下,具有相同的值。因此,这两个操作都更新了单个“测试”用户以及现有 .因此,这两个操作的结果都是一个新状态,其中只有一个“测试”用户。(这仍然会导致两次重新渲染。它们只是呈现相同的视觉信息,并且快速连续,因此您不会观察到不同的 ID。usersusers

若要实现预期行为,可以修改状态更新,以在批处理中使用当前版本的状态,而不是从上一次渲染中捕获的状态版本。例如:

setUsers(prev => [res.data, ...prev]);

评论

0赞 ramcrys 10/31/2023
是的,我知道著名的“setCount(...3 次“的例子。但在这种情况下不是这样的,对吧?第一个 setUsers 应该触发重新渲染并实际更新状态,因为它位于此处完成的 onClick 事件处理程序中(因此,第一个 React 更新批处理完成)。当 promise 解决时,它应该是另一个 React 更新批处理,并且用户列表应该是新的,之前已经更新过了,对吧?
0赞 ramcrys 10/31/2023
真正让我感到困惑的是,如果我们添加一个“isLoading”状态变量来控制微调器。在第一个 setUsers 和 setLoading 之后,组件将重新渲染,并且所有状态变量都应更新。因此,当 promise 解析时,“users”状态变量应该是已经更新的状态变量,对吧?
0赞 David 10/31/2023
@ramcrys:“但在这种情况下不是这样的,对吧?该组件确实在两次状态更新之间重新渲染,这是事实。但第二个状态更新仍然使用相同的值,因为它是从以前的渲染中调用的。回调函数围绕该值创建了一个闭包。这并不是 React 的事情,而是闭包在 JavaScript 中的工作方式。users
0赞 ramcrys 10/31/2023
哦,是的,似乎关闭部分真的吸引了我。我仍然需要考虑一下:)