提问人:likern 提问时间:9/7/2019 更新时间:7/26/2022 访问量:23550
为什么需要带有功能更新表单的 React useState?
Why React useState with functional update form is needed?
问:
我正在阅读有关功能更新的 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>
</>
);
}
评论
count
我最近偶然发现了这个需求。例如,假设您有一个组件,该组件用一定数量的元素填充数组,并能够根据某些用户操作附加到该数组(例如,在我的情况下,当用户不断向下滚动屏幕时,我一次加载 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 钩子。有关详细信息,请点击此处 - https://css-tricks.com/using-requestanimationframe-with-react-hooks/setState
requestAnimationFrame
总之,当您这样做时,会频繁调用 的处理程序,从而导致不正确的值。另一方面,使用会产生正确的值。requestAnimationFrame
count
setCount(count+delta)
setCount(prevCount => prevCount + delta)
我已经回答了类似的问题,它被关闭了,因为这是规范的问题 - 我不知道,在查看答案后,我决定在这里重新发布我的答案,因为我认为它增加了一些价值。
如果更新依赖于在状态中找到的先前值,则应使用函数形式。如果你在这种情况下不使用函数式形式,那么你的代码会在某个时候中断。
为什么会坏,什么时候坏
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>
</>
);
}
“状态更新在 React 中是异步的”答案具有误导性,下面的一些评论也是如此。在我进一步深入研究之前,我的想法也是错误的。你是对的,这很少需要。
功能状态更新背后的关键思想是,新状态所依赖的状态可能已过时。状态是如何过时的?让我们消除一些关于它的神话:
- 神话:在事件处理期间,可以在您的领导下更改状态。
- 事实:ECMAScript 事件循环一次只运行一件事。如果正在运行处理程序,则没有其他任何处理程序与它一起运行。
- 神话:快速单击两次(或任何其他快速发生的用户操作)可能会导致第二个处理程序以过时状态运行。
- 事实:React 保证不会跨多个用户发起的事件进行批量更新。即使在 React 18 中也是如此,它比以前的版本做了更多的批处理。您可以依赖于在事件处理程序之间进行渲染。
注意:React 只在通常安全的情况下批量更新。例如,React 确保对于每个用户发起的事件(如点击或按键),DOM 在下一个事件之前完全更新。例如,这确保了在提交时禁用的表单不能提交两次。
这是如何工作的?2 次快速点击 = 2 次运行相同的处理程序,对吧?
@Ich评论中提出了一个很好的问题。即使您在处理一个事件和下一个事件之间重新渲染,它们不是都使用引用相同状态的相同处理程序吗,因为它们源自同一个渲染?
这涉及到他们不经常谈论的 React 事件处理的一个细节。它在 React 17 发行说明中有所提及。
在 React 17 中,事件处理程序不再在 上注册,而是在根节点上注册。我记得读到这篇文章时感到困惑。为什么事件处理程序首先出现在为什么当我们编写 时,处理程序不在 DOMElement 上?document
document
<button onClick="...">
button
答案似乎是(除其他外)React 无法立即安全地运行处理程序。事实上,它不一定知道要运行哪个处理程序(如果有的话)!romain-trotard 的这个要点进入了实现细节。
长话短说
无论你的树中有多少个 s,React 都只在树的顶部注册一个处理程序。而这个处理程序只是一个填充码,它将事件调度到 React 中的队列中。onClick
click
这样 React 就可以按照自己的节奏处理事件。每个渲染最多只能处理 1 个用户发起的事件。队列中的其他人必须等待下一次渲染,或者之后的渲染,等等。这样,他们每个人都会得到一个新的处理程序。
例
假设您有一个递增状态的按钮。你有一个涡轮鼠标,每 1 毫秒点击一次,在下一次渲染之前你敲击该按钮 10 次。
React 在其事件队列中放置了 10 个点击事件。它会取消其中一个排队并处理它,从而导致重新渲染。
重新渲染完成后,React 会注意到队列不是空的,并将下一个队列排出队列并使用使用新状态的新处理程序进行处理。它再次重新渲染。
重复此操作,直到队列为空。最后,有 10 个渲染和 10 个不同的处理程序,具有不同的状态闭包。
好的,React 非常努力地防止过时状态。那么,你什么时候会变得陈旧呢?
以下是我能想到的主要 3 种情况:
同一处理程序中的多个状态更新
这是已经提到的情况,即在同一处理程序中多次设置相同的状态,并依赖于以前的状态。正如你所指出的,这个案例是相当人为的,因为这显然是错误的:
<button
onClick={() => {
setCount(count + 1);
setCount(count + 1);
}}
>+</button>
一个更合理的情况是调用多个函数,每个函数都在同一状态上进行更新,并依赖于前一个状态。但这仍然很奇怪,进行所有计算然后设置一次状态会更有意义。
处理程序中的异步状态更新
例如:
<button
onClick={() => {
doSomeApiCall().then(() => setCount(count + 1));
}}
>+</button>
这并不是那么明显的错误。在调用和解决之间可以更改状态。在这种情况下,状态更新确实是异步的,但你是这样做的,而不是 React!doSomeApiCall
函数形式解决了这个问题:
<button
onClick={() => {
doSomeApiCall().then(() => setCount((currCount) => currCount + 1));
}}
>+</button>
在 useEffect 中更新状态
G Gallegos 的回答一般指出了这一点,而 letvar 的回答指出了这一点。如果您要根据 中的先前状态更新状态,则将该状态放在依赖项数组中(或不使用依赖项数组)是无限循环的秘诀。请改用函数式窗体。useEffect
useEffect
requestAnimationFrame
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 针对这种情况和类似情况引入了批处理。这是一项有用的性能改进。缺点是它破坏了依赖于上述行为的类组件。
引用
- React 工作组讨论
- React 17 发行说明
- romain-trotard 关于 React 事件处理的要点
- Ehab 的回答,其中还提到了需要功能更新的两种情况。
评论
<button onClick={() => setCount(count + 1)}>+</button>
counter
评论