赞
踩
对于那些来自Redux背景的人来说,useReducer
看起来似乎复杂而不必要。在useState
和上下文之间,很容易陷入这样的陷阱:对于大多数较简单的用例,Reducer会增加不必要的复杂性。但是,事实证明useReducer
可以大大简化状态管理。让我们看一个例子。
与我的其他帖子一样,此代码来自我的书目项目。用例是屏幕允许用户扫描书籍。记录ISBN,然后将其发送到一个查找图书信息的限速服务。由于查找服务受速率限制,因此无法保证您的图书很快就会被查找,因此使用 websocket;随着数据更新的到来,消息将沿着ws发送,并在 ui 中进行处理。ws 的 api 很简单:数据包具有_messageType
属性,其余对象用作有效负载。显然,更认真的项目会设计出更可靠的产品。
使用组件类,用于设置 ws 的代码非常简单:在componentDidMount
ws 订阅中创建了一个订阅,然后将 componentWillUnmount
其拆了。考虑到这一点,很容易陷入尝试使用 hooks 进行跟踪的陷阱
- const BookEntryList = props => {
- const [pending, setPending] = useState(0);
- const [booksJustSaved, setBooksJustSaved] = useState([]);
-
- useEffect(() => {
- const ws = new WebSocket(webSocketAddress("/bookEntryWS"));
-
- ws.onmessage = ({ data }) => {
- let packet = JSON.parse(data);
- if (packet._messageType == "initial") {
- setPending(packet.pending);
- } else if (packet._messageType == "bookAdded") {
- setPending(pending - 1 || 0);
- setBooksJustSaved([packet, ...booksJustSaved]);
- } else if (packet._messageType == "pendingBookAdded") {
- setPending(+pending + 1 || 0);
- } else if (packet._messageType == "bookLookupFailed") {
- setPending(pending - 1 || 0);
- setBooksJustSaved([
- {
- _id: "" + new Date(),
- title: `Failed lookup for ${packet.isbn}`,
- success: false
- },
- ...booksJustSaved
- ]);
- }
- };
- return () => {
- try {
- ws.close();
- } catch (e) {}
- };
- }, []);
-
- //...
- };

我们将 ws 创建放在useEffect
带有空依赖项列表的调用中,这意味着它永远不会重新触发,并且我们返回一个函数来进行卸载。第一次安装组件时,将建立 ws,而在卸载组件时,将其拆除,就像使用类组件一样。
此代码严重失败。我们正在访问 useEffect
闭包内部的状态,但不将该状态包含在依赖项列表中。例如,内部 useEffect
的值 pending
绝对将始终为零。当然,我们可能会 setPending
在 ws.onmessage
处理程序内部调用,这将导致状态更新,并且组件将重新呈现,但是当它重新呈现时,我们 useEffect
将不会重新触发(同样,由于依赖列表为空)结果,关闭将继续关闭的当前值 pending
。
需要明确的是,使用下面讨论的“(Hooks)”规则可以很容易地发现这一点。从根本上说,要打破旧习惯中的使用类组件的思维。不要想着从componentDidMount
/ componentDidUpdate
/ componentWillUnmount
的框架中访问这些依赖项列表。仅仅因为这个类组件版本会在componentDidMount中设置一次webSocket,并不意味着您可以直接转换为具有空依赖项列表的useffect调用。
不要想太多,也不要太聪明:effect回调中使用的渲染函数作用域中的任何值都需要添加到依赖项列表中:这包括props,state等。
虽然我们可以将所有需要的状态添加到我们的 useEffect
依赖项列表中,但这将导致 WebSocket 被卸载,并在每次更新时重新创建。如果 ws 在创建时发送了初始状态的数据包(可能已经在我们的用户界面中进行了说明和更新),这将很难有效,并且可能实际上会引起问题。
但是,如果我们仔细观察,可能会发现一些有趣的事情。我们正在执行的每个操作始终处于先验状态。我们一直在说诸如“增加待处理书的数量”,“将这本书添加到已完成的清单中”之类的意思。实际上,发送将先前状态投影到新状态的命令是 reducer 的全部目的。
将整个状态管理移至 reducer 将消除对 useEffect
回调中对本地状态的任何引用;让我们看看怎么做。
- function scanReducer(state, [type, payload]) {
- switch (type) {
- case "initial":
- return { ...state, pending: payload.pending };
- case "pendingBookAdded":
- return { ...state, pending: state.pending + 1 };
- case "bookAdded":
- return {
- ...state,
- pending: state.pending - 1,
- booksSaved: [payload, ...state.booksSaved]
- };
- case "bookLookupFailed":
- return {
- ...state,
- pending: state.pending - 1,
- booksSaved: [
- {
- _id: "" + new Date(),
- title: `Failed lookup for ${payload.isbn}`,
- success: false
- },
- ...state.booksSaved
- ]
- };
- }
- return state;
- }
- const initialState = { pending: 0, booksSaved: [] };
-
- const BookEntryList = props => {
- const [state, dispatch] = useReducer(scanReducer, initialState);
-
- useEffect(() => {
- const ws = new WebSocket(webSocketAddress("/bookEntryWS"));
-
- ws.onmessage = ({ data }) => {
- let packet = JSON.parse(data);
- dispatch([packet._messageType, packet]);
- };
- return () => {
- try {
- ws.close();
- } catch (e) {}
- };
- }, []);
-
- //...
- };

虽然行数稍多,但我们不再具有多个更新的函数(functions),我们的 useEffect
主体更加简单易读,并且不再需要担心过时的状态被封闭在闭包中:我们所有的 update 都是通过 dispatch 分派单独的 reducer。这也有助于提高可测试性,因为我们的 reducer 非常容易测试。它只是一个普通的 JavaScript 函数。正如 React 团队的 Sunil Pai 所说,使用 reducer 有助于将读、写入分离开来。我们的useffect主体现在只担心发送操作,这会产生新状态;在它既关注读取现有状态,又关注写入新状态之前。
您可能已经注意到将 actions 作为了带有 type 插槽的数组,而不是作为带有 key 的对象发送到 reducer。这只是丹·阿布拉莫夫教给我的一个小技巧,让我把样板文件减少一点:)。
最后,有些人可能想知道为什么在原始代码中,我不只是这样做
setPending(pending => pending - 1 || 0);
而不是
setPending(pending - 1 || 0);
这样就可以解决关闭问题,并且对于这个特定用例可以很好地工作;但是,如果booksJustSaved
需要对的值进行微小的更新(pending
反之亦然),则此解决方案将无法正常运行,从而使我们始终处于起点。此外,我发现 reducer 版本更干净一些,状态管理在其自己的 reducer 功能中很好地分开了。
总而言之,我认为 useReducer()
目前利用不足。它远没有您想象的那么可怕。试试看!
编码愉快!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。