提高React开发效率的神器-immer
更新复杂对象的噩梦
在 React 类组件中,state是一个对象,当组件相对复杂时,state对象的结构可能也相当复杂。
比如
1 | class User extends Component { |
假如用户在这个组件中可以更改个人资料 —— 如更改某个输入框内容就能更改所在城市的地区和邮编号码, 正规的传统的标准的写法如下
1 | this.setState((prevState) => { |
为什么得这么写?
- React 不允许直接更改state ,而应该使用 setState
- setState 会合并更改(merge update),所以不需要手写完整的state,但是合并仅限于对象属性的第一级
- setState 会 **异步 **地触发re-render,所以不要直接依赖 this.state (此时的 http://this.state.xxx 不一定是彼时的 http://this.state.xxx),即上面的写法是有潜在bug的
- 这三点的详细讨论可以参考React官方文档: https://reactjs.org/docs/state-and-lifecycle.html#using-state-correctly
在上面的情景中,传统的写法已经十分繁琐了,而且有明显 代码噪音 —— 尽管代码表达的是更改 area 和 postcode 却不得不看到大量的扩展语法
更糟糕的是,容易出错 —— 每深入对象一层,扩展语法后的路径也需要再进一层(如 …prevState.address.city) ,在复制粘贴过程中弄错/弄丢路径是屡见不鲜的。
当 state对象结构的层级更深 的时候,改动最深层的state子节点写起来会更麻烦。
另一个类似的且常见的场景是 Redux reducer
1 | const tagReducer = function(state, action){ |
为什么不能直接更改 reducer 的参数 state ?
reducer 必须是纯函数,不能更改参数state,而要计算并返回下一个state,所以不得不这么写。
简单总结一下目前讨论的困境 ——
当我们需要更新某一个复杂的js对象的某个深层属性(子节点),且不允许直接更改这个js对象,单纯借助ES6展开语法导致代码非常繁琐且易出错。
解决方案
我们可以想出一个简单的解决方案 —— 先深拷贝出一个新的对象,然后直接更改新对象的属性
1 | this.setState((prevState) => { |
但是,这种方案有明显的性能问题 —— 不管打算更新对象的哪一个属性(子节点),每次都不得不深拷贝整个对象;当对象特别大的时候,深拷贝会导致性能问题。
终于到主角 immer 出场了!
1 | import {produce} from 'immer'; |
不够香?那试试这个写法
1 | this.setState(produce(draftState => { |
更重要的是,在 immer 的背后做了性能优化,而不是简单的全部深度拷贝,所以不用担心性能问题。
immer 还默认地把返回的新对象设置成了不可变对象,从而避免人为意外地直接修改。当然,如果不想要这个行为,可以调用 immer 的setAutoFreeze(false) 取消掉。