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



我正在阅读有关功能更新的 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>
</>
);
}

直接更新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>
</>
);
}

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

状态更新在 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>
</>
);
}

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>
</>
);
}

我最近偶然发现了对这个的需求。例如,假设您有一个组件,该组件用一定数量的元素填充数组,并且能够根据某些用户操作附加到该数组(就像在我的例子中一样,当用户不断向下滚动屏幕时,我一次加载 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>
...

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

救援功能更新:

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

"状态更新在 React 中是异步的"答案具有误导性,它下面的一些评论也是如此。在我进一步深入研究之前,我的想法也是错误的。你是对的,这很少需要。

功能状态更新背后的关键思想是,新状态所依赖的状态可能已过时。状态如何变得陈旧?让我们消除一些关于它的神话:

  • 误解:在事件处理期间,状态可以在您之下更改。
    • 事实:ECMAScript 事件循环一次只运行一件事。如果您正在运行处理程序,则没有其他内容与它一起运行。
  • 误解:快速单击两次(或任何其他用户操作快速发生)可能会导致来自两个处理程序调用的状态更新被批处理。
    • 事实:React 保证不会在多个用户发起的事件中批量更新。即使在 React 18 中也是如此,它比以前的版本执行更多的批处理。您可以依赖在事件处理程序之间具有呈现。

来自 React 工作组:

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

那么什么时候会出现陈旧状态呢?

以下是我能想到的主要 3 个案例:

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

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

<button
onClick={() => {
setCount(count + 1);
setCount(count + 1);
}}
>+</button>

一个更合理的情况是调用多个函数,每个函数都在同一状态上更新并依赖于以前的状态。但这仍然很奇怪,做所有的计算然后设置一次状态会更有意义。

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

例如:

<button
onClick={() => {
doSomeApiCall().then(() => setCount(count + 1));
}}
>+</button>

这显然没有错。可以在调用doSomeApiCall和解析之间更改状态。在这种情况下,状态更新确实是异步的,但是这样做的,而不是 React!

功能表单修复了此问题:

<button
onClick={() => {
doSomeApiCall().then(() => setCount((currCount) => currCount + 1));
}}
>+</button>

更新使用中的状态效果

G Gallegos的回答为useEffect指出了这一点,而letvar的回答指出了这一点,useEffectrequestAnimationFrame。如果要根据useEffect中的先前状态更新状态,则将该状态放在依赖项数组中(或不使用依赖项数组)是无限循环的秘诀。请改用功能窗体。

总结

您不需要基于先前状态的状态更新的功能形式,只要您这样做 1. 在用户触发的事件处理程序中 2. 每个状态的每个处理程序一次,3. 同步。如果违反任何这些条件,则需要功能更新。

有些人可能更喜欢始终使用功能更新,因此您不必担心这些条件。其他人可能更喜欢在安全的情况下使用较短的形式以使其清晰,这对于许多处理程序来说都是如此。在这一点上,它是个人偏好/代码风格。

历史笔记

我在 Hooks 之前学习了 React,当时只有类组件才有状态。在类组件中,"同一处理程序中的多个状态更新"看起来并没有那么明显的错误:

<button
onClick={() => {
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
}}
>+</button>

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

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

setTimeout(() => {
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
}, 1000);

由于它不是事件处理程序,因此 React 会在每次调用setState后重新渲染。

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

引用

  • 反应工作组讨论
  • ehab 的回答,其中还提到了需要功能更新的两种情况。

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

如果更新依赖于在状态中找到的先前值,则应使用功能窗体。如果您在这种情况下不使用函数形式,那么您的代码将在某个时候中断。

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

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>
</>
);
};

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

const App = () => {
const [counter, setCounter] = useState(0);
return (
<>
<p>counter {counter} </p>
<button
onClick={() => {
setCounter(counter + 1);
setCounter(counter + 1);
}}
>
Add twice
</button>

</>
);
}

使用setState功能更新的另一个用例 -requestAnimationFrame使用 react 钩子。详细信息可在此处获得 - https://css-tricks.com/using-requestanimationframe-with-react-hooks/

总之,当您执行setCount(count+delta)时,requestAnimationFrame的处理程序会频繁被调用,从而导致不正确的count值。另一方面,使用setCount(prevCount => prevCount + delta)会产生正确的值。

相关内容

  • 没有找到相关文章

最新更新