为什么需要具有功能更新表单的 React useState?

lik*_*ern 25 reactjs react-hooks

我正在阅读有关功能更新的React Hook 文档并查看此引用:

“+”和“-”按钮使用函数形式,因为更新的值是基于之前的值

但是我看不出需要功能更新的目的是什么,它们与直接使用旧状态计算新状态有什么区别。

为什么 React useState Hook 的更新器函数根本需要函数式更新表单? 我们可以清楚地看到差异的示例有哪些(因此使用直接更新会导致错误)?

例如,如果我从文档中更改此示例

function Counter({initialCount}) {
  const [count, setCount] = useState(initialCount);
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(initialCount)}>Reset</button>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
    </>
  );
}
Run Code Online (Sandbox Code Playgroud)

count直接更新:

function Counter({initialCount}) {
  const [count, setCount] = useState(initialCount);
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(initialCount)}>Reset</button>
      <button onClick={() => setCount(count + 1)}>+</button>
      <button onClick={() => setCount(count - 1)}>-</button>
    </>
  );
}
Run Code Online (Sandbox Code Playgroud)

我看不出任何行为差异,也无法想象计数不会更新(或不会更新)的情况。因为每当计数发生变化时,onClick就会调用新的闭包 for ,捕获最新的count.

Ale*_*sen 21

React 中的状态更新是异步的。因此,count下次更新时可能会有旧值。例如,比较这两个代码示例的结果:

function Counter({initialCount}) {
  const [count, setCount] = useState(initialCount);
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(initialCount)}>Reset</button>
      <button onClick={() => {
        setCount(prevCount => prevCount + 1); 
        setCount(prevCount => prevCount + 1)}
      }>+</button>
    </>
  );
}
Run Code Online (Sandbox Code Playgroud)

function Counter({initialCount}) {
  const [count, setCount] = useState(initialCount);
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(initialCount)}>Reset</button>
      <button onClick={() => {
        setCount(count + 1); 
        setCount(count + 1)}
      }>+</button>
    </>
  );
}
Run Code Online (Sandbox Code Playgroud)

  • 这正是原因 - 用户可以在重新渲染组件之前单击 + 和 - 或非常快地单击 + 两次,并且在第二次调用中“count”的值将是错误的(未更新)。 (4认同)
  • 快速点击不会弄乱你的状态,并且 Hooks hello world 也不会出错。查看我的答案:/sf/answers/5118112961/ (3认同)

Van*_*pps 19

\xe2\x80\x9cstate更新在 React\xe2\x80\x9d 答案中是异步的,下面的一些评论也是如此。在我进一步深入研究之前,我的想法也是错误的。你是对的,这很少需要。

\n

功能状态更新背后的关键思想是,新状态所依赖的状态可能已经过时。状态如何变得陈旧?让\xe2\x80\x99s 消除一些关于它的误解:

\n
    \n
  • 神话:在事件处理期间,您可以更改状态。\n
      \n
    • 事实: ECMAScript 事件循环一次只运行一件事。如果您正在运行一个处理程序,则没有其他任何东西可以与它一起运行。
    • \n
    \n
  • \n
  • 神话:快速单击两次(或快速发生的任何其他用户操作)可能会导致第二个处理程序以陈旧状态运行。\n
      \n
    • 事实: React 保证不会在多个用户启动的事件中批量更新。即使在 React 18 中也是如此,它比以前的版本进行了更多的批处理。您可以依靠在事件处理程序之间进行渲染。
    • \n
    \n
  • \n
\n

来自 React 工作组

\n
\n

注意:React 仅在通常安全的情况下批量更新。例如,React 确保对于每个用户启动的事件(例如单击或按键),DOM 在下一个事件之前完全更新。例如,这可以确保禁用提交的表单可以\xe2\x80\x99 提交两次。

\n
\n

这是如何运作的?2 次快速点击 = 同一处理程序的 2 次运行,对吗?

\n

@Ich 在评论中提出了一个很好的问题。即使您在处理一个事件和下一个事件之间重新渲染,它们是否都使用引用相同状态的相同处理程序,因为它们源自相同的渲染?

\n

这涉及到他们不常谈论的 React 事件处理的细节。React 17 发行说明中提到了这一点。

\n

在 React 17 中,事件处理程序不再注册在 上document,而是注册在根节点上。我记得读过这篇文章并感到困惑。document为什么首先要使用事件处理程序?为什么当我们编写 时<button onClick="...">,处理程序不在DOMElement上button

\n

答案似乎是(除其他外)React 无法立即安全地运行处理程序。事实上,它不一定知道要运行哪个处理程序(如果有的话)!romain-trotard 的要点涉及到实现细节。

\n

长话短说

\n

无论onClick树中有多少个,React 都只会click在树的顶部注册一个处理程序。该处理程序只是一个垫片,它将事件分派到 React 中的队列中。

\n

这样 React 就可以按照自己的节奏处理事件。每个渲染最多只能处理 1 个用户启动的事件。队列中的其他人必须等待下一次渲染,或者之后的渲染,等等。这样,他们每个人都会得到一个新的处理程序。

\n

例子

\n

假设您有一个增加状态的按钮。您有一个涡轮鼠标,每 1 毫秒点击一次,并且在下一次渲染之前敲击该按钮 10 次。

\n

React 将 10 个点击事件放入其事件队列中。它将其中一个出列并处理它,从而导致重新渲染。

\n

重新渲染完成后,React 会注意到队列不为空,并将下一个队列出队并使用使用新状态的新处理程序来处理它。它再次重新渲染。

\n

重复此操作直到队列为空。最后有 10 个渲染和 10 个具有不同状态闭包的不同处理程序。

\n

好吧,React 非常努力地防止过时状态。那么,什么时候你会变得陈旧状态呢?

\n

我能想到的主要有3种情况:

\n

同一处理程序中的多个状态更新

\n

这是已经提到的情况,您在同一个处理程序中多次设置相同的状态,并依赖于先前的状态。正如您所指出的,这种情况非常人为,因为这显然看起来是错误的:

\n
  <button\n    onClick={() => {\n      setCount(count + 1);\n      setCount(count + 1);\n    }}\n  >+</button>\n
Run Code Online (Sandbox Code Playgroud)\n

更合理的情况是调用多个函数,每个函数都会更新同一状态并依赖于先前的状态。但是\xe2\x80\x99s仍然很奇怪,\xe2\x80\x99d进行所有计算然后设置一次状态更有意义。

\n

处理程序中的异步状态更新

\n

例如:

\n
  <button\n    onClick={() => {\n      doSomeApiCall().then(() => setCount(count + 1));\n    }}\n  >+</button>\n
Run Code Online (Sandbox Code Playgroud)\n

这并不是那么明显的错误。doSomeApiCall在您调用和解决之间可以更改状态。在这种情况下,状态更新确实是异步的,但是这样做的,而不是 React!

\n

函数形式解决了这个问题:

\n
  <button\n    onClick={() => {\n      doSomeApiCall().then(() => setCount((currCount) => currCount + 1));\n    }}\n  >+</button>\n
Run Code Online (Sandbox Code Playgroud)\n

更新 useEffect 中的状态

\n

G Gallegos 的回答指出了这一点useEffect,而letvar 的回答则指出了这useEffect一点requestAnimationFrame。如果您要根据 中的先前状态更新状态useEffect,则将该状态放入依赖项数组中(或不使用依赖项数组)会导致无限循环。请改用函数形式。

\n

概括

\n

您不需要\xe2\x80\x99t 需要基于先前状态进行状态更新的功能形式,只要您执行以下操作:1. 在用户触发的事件处理程序中2. 每个处理程序每​​个状态一次3. 同步。如果您违反了其中任何一个条件,则需要进行功能更新。

\n

有些人可能更喜欢始终使用功能更新,因此您不必担心这些情况。为了清楚起见,其他人可能更喜欢较短的形式,因为这样做是安全的,这对于许多处理程序来说都是如此。此时\xe2\x80\x99是个人喜好/代码风格。

\n

历史记录

\n

我在 Hooks 之前学习了 React,当时只有类组件才有状态。在类组件中,\xe2\x80\x9c在同一个处理程序中进行多次状态更新\xe2\x80\x9d看起来\xe2\x80\x99t看起来显然是错误的:

\n
  <button\n    onClick={() => {\n      this.setState({ count: this.state.count + 1 });\n      this.setState({ count: this.state.count + 1 });\n    }}\n  >+</button>\n
Run Code Online (Sandbox Code Playgroud)\n

由于状态是实例变量而不是函数参数,因此这看起来很好,除非您知道setState在同一处理程序中批量调用。

\n

事实上,在 React <= 17 中,这可以正常工作:

\n
  setTimeout(() => {\n    this.setState({ count: this.state.count + 1 });\n    this.setState({ count: this.state.count + 1 });\n  }, 1000);\n
Run Code Online (Sandbox Code Playgroud)\n

由于它\xe2\x80\x99 不是事件处理程序,React 在每次调用后都会重新渲染setState

\n

React 18 针对这种情况和类似情况引入了批处理。这是一个有用的性能改进。缺点是它破坏了依赖上述行为的类组件。

\n

参考

\n\n


eha*_*hab 10

我已经回答了类似的问题,它已经关闭,因为这是规范的问题 - 我不知道,在查看答案后,我决定在这里重新发布我的答案,因为我认为它增加了一些价值。

如果您的更新取决于在状态中找到的先前值,那么您应该使用函数形式。如果在这种情况下您不使用函数形式,那么您的代码有时会崩溃。

为什么会破裂以及何时破裂

React 功能组件只是闭包,闭包中的状态值可能已过时 - 这意味着闭包内的值与该组件的 React 状态值不匹配,这可能发生在以下情况:

1-异步操作(在本例中单击“慢速添加”,然后多次单击“添加”按钮,稍后您将看到单击“慢速添加”按钮时状态已重置为闭包内的状态)

const App = () => {
  const [counter, setCounter] = useState(0);

  return (
    <>
      <p>counter {counter} </p>
      <button
        onClick={() => {
          setCounter(counter + 1);
        }}
      >
        immediately add
      </button>
      <button
        onClick={() => {
          setTimeout(() => setCounter(counter + 1), 1000);
        }}
      >
        Add
      </button>
    </>
  );
};
Run Code Online (Sandbox Code Playgroud)

2-当您在同一个闭包中多次调用更新函数时

const App = () => {
  const [counter, setCounter] = useState(0);

  return (
    <>
      <p>counter {counter} </p>
      <button
        onClick={() => {
          setCounter(counter + 1);
          setCounter(counter + 1);
        }}
      >
        Add twice
      </button>
   
    </>
  );
}
Run Code Online (Sandbox Code Playgroud)


G G*_*gos 8

我最近偶然发现了这个需求。例如,假设您有一个组件,该组件用一定数量的元素填充数组,并且能够根据某些用户操作附加到该数组(例如在我的情况下,我以用户身份一次加载 10 个项目)继续向下滚动屏幕。代码看起来有点像这样:

function Stream() {
  const [feedItems, setFeedItems] = useState([]);
  const { fetching, error, data, run } = useQuery(SOME_QUERY, vars);

  useEffect(() => {
    if (data) {
      setFeedItems([...feedItems, ...data.items]);
    }
  }, [data]);     // <---- this breaks the rules of hooks, missing feedItems

...
<button onClick={()=>run()}>get more</button>
...

Run Code Online (Sandbox Code Playgroud)

显然,您不能只将 feedItems 添加到 useEffect 钩子中的依赖项列表,因为您在其中调用了 setFeedItems,因此您会陷入循环。

救援功能更新:

useEffect(() => {
    if (data) {
      setFeedItems(prevItems => [...prevItems, ...data.items]);
    }
  }, [data]);     //  <--- all good now
Run Code Online (Sandbox Code Playgroud)

  • 这是我需要的答案......正在处理合并数组 (3认同)