You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
when we update a state variable, we replace its value. This is different from this.setState in a class, which merges the updated fields into the object
import{useImmer}from"use-immer";functionApp(){const[nestedObj,updateNestedObj]=useImmer({name: "Niki de Saint Phalle",artwork: {title: "Blue Nana",city: "Hamburg",image: "https://i.imgur.com/Sd1AgUOm.jpg",},});constupdateCity=(city)=>{updateNestedObj((draft)=>{draft.artwork.city=city;});};}
immer 的实现基于 Proxy 和不可变状态,结合 React 使用时多少有些别扭,有没有更简单的方式呢?
大家好,我是 Monch,今天想跟大家分享的是,如何在 React Hooks 中更优雅地更新复杂的状态数据,这里的复杂状态可能是,
Object
对象Object
对象相信大家在日常开发中都会遇到如下的场景,比如一个分页器对象可能由以下几部分组成,
定义分页器的状态时,更好的做法是将上面的属性值合并为一个
pagination
对象,而不是分别定义,这样的状态定义带来的一个弊端是,如果我们只需要更新其中的部分属性值,为了保留状态对象的其他属性,我们需要浅拷贝一次,再合并需要更新的属性值。比如下面我们需要在
pagination
的页码和页大小改变时,更新分页器状态,如果这个复杂状态是一个嵌套对象(Nested Object),那看起来就更糟糕了,我们需要逐层拷贝,一直到待更新属性所在的层级,
实际场景中可能不会有这么深的层级,但是嵌套对象的场景是确实存在的。如果你的状态定义是一个嵌套对象,那么你很可能需要优先考虑将它拆分为多个状态,而不是一直嵌套下去,或者使用我们接下来介绍的一些方式。
useLegcyState
众所周知,不同于 ”class 时代“ 类组件 this.setState 的自动合并,在
hooks
中我们通过useState
定义的状态,调用dispatcher
更新时,React 不会帮我们自动合并,而是直接替换,为了避免每次都需要拷贝对象,我们可以考虑自己实现一个自定义
hook
辅助进行属性值的自动合并,这样一来,更新分页器时,只需要传入待更新的属性值就可以了,
看起来还不错,我们还可以考虑对嵌套对象提供支持,拷贝时逐层地遍历嵌套对象,找到合适的位置更新属性值,感兴趣的同学可以自己尝试封装一下。对于嵌套状态的更新,其实社区很早就有其他版本的方案,比如 immer。
useImmer
Immer
是 Mobx 的作者 mweststrate 在 2018 年 2 月发布的一个支持不可变状态的库,核心原理基于 JavaScript 的 Proxy 对象,支持柯里化,状态经过Immer
后会被代理为draft
,对draft
的修改会生成不可变状态,mweststrate 后面也提供了对应的 React hook 版本的实现 useImmer,经过
useImmer
包装后的draft
是一个响应式对象,通过draft
对象来修改状态,就可以避免手动进行深拷贝和合并操作,immer
的实现基于Proxy
和不可变状态,结合 React 使用时多少有些别扭,有没有更简单的方式呢?其实,我们完全可以去掉不可变状态,仅基于响应式实现来处理复杂的状态数据更新。
useReactive
我们来构想一下这个
useReactive
的 hook,它的用法和useState
类似,但可以动态地设置值,实现上,我们如何将状态数据变成响应式并且与 React 结合呢?需要考虑下面的两个问题,
state.count = 1
设置后,如何让真实的状态state.count
变成 1 ?observer
针对第
1
点,前面我们介绍了可以使用Proxy
来包装我们的状态对象,实现上,我们考虑用一个observer
函数来将状态对象变成响应式对象,需要注意如果状态对象是深层嵌套的,需要对每一层都进行代理,observer
接受一个初始的状态对象initialState
以及一个回调函数,返回一个Proxy
对象,我们劫持了代理对象的getter
和setter
,在读取代理对象的值时,将其包装为响应式对象,设置值时执行回调函数。有了observer
,接下来就是如何与 React 视图结合。forceUpdate
针对第
2
点,在触发更新后,React hooks 语法下如何让视图也进行刷新?是不是只需要利用
useState
或useReducer
这类hook
的原生能力即可,我们调用第二个返回值的dispatch
函数,触发状态改变就可以让当前组件强制刷新,这里我们选择useReducer
,将dispatch
函数直接命名为forceUpdate
,解决了上述两个问题,我们的
useReactive
就基本实现了,只需要在代理对象设置值时调用forceUpdate
触发视图更新即可,同时作为一个通用 hook,可以考虑使用useMemo
对包装后的响应式对象进行缓存,上面的代码已经满足我们的需求了,为了避免 React Hooks 的闭包陷阱,我们还可以考虑对状态对象
initialState
做一层处理,始终代理最新的状态。useLatest
避免闭包问题的思路就是永远返回最新的值,实现上,我们可以使用
useRef
对值进行缓存,使用
useLatest
优化后,最终的useReactive
实现如下,我们写一个经典的计数器例子来验证下
useReactive
,你可以点击 这里 查看
useReactive
的效果。实际上,上述的响应式 hook 基本就是 ahooks 里 useReactive 的实现,不过
ahooks
还考虑了,plainObject
和数组进行代理useCreation
进行缓存,可以理解为增强的useRef
和useMemo
,缓存值保持最新值,避免实例化的性能隐患如果你需要在生产环境尝试
useReactive
,建议直接使用ahooks
。小结
React Hooks 时代,对于复杂状态数据的更新,我们可以考虑,
写在最后
本文首发于我的 博客,才疏学浅,难免有错误,文章有误之处还望不吝指正!
如果有疑问或者发现错误,可以在评论区进行提问和勘误,
如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。
The text was updated successfully, but these errors were encountered: