immerjs:React开发必会技能

发布时间 2023-09-25 15:42:54作者: 漫思

immerjs:React开发必会技能

 

我们都知道React追求的泛式是数据不可变,一般情况下state或者props改变才进入render阶段;如果我们创建的state是一个一般数据类型,他就是一个不可变的值,如果需要改变我们需要重新创建一个state去覆盖它;但是如果我们的state是一个对象,我们在原对象上对其进行修改;有时候你会发现并不触发render,所以这里我们需要传入一个新的不可变对象;一般来讲直接用解决深拷贝的问题的方法就能解决;但是这种方式并不被官方推荐;因此我们需要借助immutable.js或者immer.js去生成一个不可变对象;

我们先看一个例子:
```
import react, {useState} from 'react'
export default function IndexPage() {
const [list, setList] = useState([
{
id:1,
value:'大饼'
},
{
id:2,
value:'豆浆'
},
]);
const add = ()=>{
list.push({
id:3,
value:'油条'
})
console.log(list.length)
setList(list)
}
return (
<div>
{list.map(item=><div key={item.id}>{item.value}</div>)}
<button onClick={add}>add</button>
</div>
);
}

```
在这个案例中运行并点击你会发现,我们通过方法list的对象方法push给list插入一条值改变list的值,控制台会输出list的长度为3,然后我们将list传给setList并运行,但是我们的界面依旧没改变,依旧为运行前的大饼和豆浆;那是因为我们并没有创建一个新值覆盖原有的list只是在原对象上进行修改;而react并不能监听到这个变化;所以导致页面没有更新;所以需要去改变原来的list进行覆盖;一般我们会这么做;


```
import react, {useState} from 'react'
// import produce from 'immer'
export default function IndexPage() {
const [list, setList] = useState([
{
id:1,
value:'大饼'
},
{
id:2,
value:'豆浆'
},
]);
const add = ()=>{
setList([...list,{
id:3,
value:'油条'
}])
}
return (
<div>
{list.map(item=><div key={item.id}>{item.value}</div>)}
<button onClick={add}>add</button>
</div>
);
}

```
或者
```
import react, {useState} from 'react'
import produce from 'immer'
export default function IndexPage() {
const [list, setList] = useState([
{
id:1,
value:'大饼'
},
{
id:2,
value:'豆浆'
},
]);
const add = ()=>{
list.push({
value:'油条',
id:3
})
setList(list.map(item=>item))
}
return (
<div>
{list.map(item=><div key={item.id}>{item.value}</div>)}
<button onClick={add}>add</button>
</div>
);
}

```
无论是使用扩展运算符还是对象方法map都是生成一个新的list去对原数据进行覆盖,当然我们也能用到其他数组的高阶对象方法例如filter,reduce等等因为这类高阶方法都有一个特性返回一个新值;但是这并不是非常符合编程习惯的,我们追求的是函数式编程;那么如果我们将这个问题交给emmerjs会怎么做?

首先我们要引入安装immer


```
Yarn: yarn add immer
NPM: npm install immer

```


```
import react, {useState} from 'react'
import produce from 'immer'
export default function IndexPage() {
const [list, setList] = useState([
{
id:1,
value:'大饼'
},
{
id:2,
value:'豆浆'
},
]);
const add = ()=>{
setList(produce(draft=>{
draft.push({
value:'油条',
id:3
})
}))
}
return (
<div>
{list.map(item=><div key={item.id}>{item.value}</div>)}
<button onClick={add}>add</button>
</div>
);
}

```
我们可以调用produce方法来生成一个新的list对原list进行一次覆盖,这样子做更加简便,并且避免修改数据的失误操作;

immer是非必须的,但是使用它肯定是因为它有好处,官方是这样子列举的

1. **遵循不可变数据范式,同时使用普通的 JavaScript 对象、数组、Sets 和 Maps。无需学习新的 API 或 "mutations patterns"!**
2. **强类型,无基于字符串的路径选择器等**
3. **开箱即用的结构共享**
4. **开箱即用的对象冻结**
5. **深度更新轻而易举**
6. **样板代码减少。更少的噪音,更简洁的代码**
7. **对 JSON 补丁的一流支持**
8. **小:3KB gzip**

那么知道了immer的好处之后我们要怎样来使用?

结合常用案例我将从以下三个点来解读;
1. **produce**
2. **柯里化 producers**
3. **React & Immer**

首先我们来讲讲immerjs中用的最多的produce方法;

----------

**produce**

使用produce 需要一个 baseState,以及一个可用于对传入的 draft 进行所有所需更改的 recipe方法然后它会返回一个nextState,直到这一步baseState依旧是原来的那个数据,但是返回的nextState就是我们想要的新值。.
例如:

```
const baseState = [1,2]
const nextState = produce(baseState, draft => {
draft.push(3)
})
console.log(nextState)//[1,2,3]
```
使用produce传入我们的原数据baseState与recipe方法,然后内部会生成一个新的nextState再把这个nextState通过proxy API传给recipe进行转换(draft参数)等recipe方法结束再将nextState返回,在此期间baseState是不变的;

了解运行原理之后你会发现很多常用的API都能通过produce来生成新的对象;官方给出了以下常用案例

**更新对象**


```
import produce from "immer"

const todosObj = {
id1: {done: false, body: "Take out the trash"},
id2: {done: false, body: "Check Email"}
}

// 添加
const addedTodosObj = produce(todosObj, draft => {
draft["id3"] = {done: false, body: "Buy bananas"}
})

// 删除
const deletedTodosObj = produce(todosObj, draft => {
delete draft["id1"]
})

// 更新
const updatedTodosObj = produce(todosObj, draft => {
draft["id1"].done = true
})
```
**更新数组**


```
import produce from "immer"

const todosArray = [
{id: "id1", done: false, body: "Take out the trash"},
{id: "id2", done: false, body: "Check Email"}
]

// 添加
const addedTodosArray = produce(todosArray, draft => {
draft.push({id: "id3", done: false, body: "Buy bananas"})
})

// 索引删除
const deletedTodosArray = produce(todosArray, draft => {
draft.splice(3 /*索引 */, 1)
})

// 索引更新
const updatedTodosArray = produce(todosArray, draft => {
draft[3].done = true
})

// 索引插入
const updatedTodosArray = produce(todosArray, draft => {
draft.splice(3, 0, {id: "id3", done: false, body: "Buy bananas"})
})

// 删除最后一个元素
const updatedTodosArray = produce(todosArray, draft => {
draft.pop()
})

// 删除第一个元素
const updatedTodosArray = produce(todosArray, draft => {
draft.shift()
})

// 数组开头添加元素
const addedTodosArray = produce(todosArray, draft => {
draft.unshift({id: "id3", done: false, body: "Buy bananas"})
})

// 根据 id 删除
const deletedTodosArray = produce(todosArray, draft => {
const index = draft.findIndex(todo => todo.id === "id1")
if (index !== -1) draft.splice(index, 1)
})

// 根据 id 更新
const updatedTodosArray = produce(todosArray, draft => {
const index = draft.findIndex(todo => todo.id === "id1")
if (index !== -1) draft[index].done = true
})

// 过滤
const updatedTodosArray = produce(todosArray, draft => {
// 过滤器实际上会返回一个不可变的状态,但是如果过滤器不是处于对象的顶层,这个依然很有用
return draft.filter(todo => todo.done)
})
```
**嵌套数据结构**


```
import produce from "immer"

// 复杂数据结构例子
const store = {
users: new Map([
[
"17",
{
name: "Michel",
todos: [
{
title: "Get coffee",
done: false
}
]
}
]
])
}

// 深度更新
const nextStore = produce(store, draft => {
draft.users.get("17").todos[0].done = true
})

// 过滤
const nextStore = produce(store, draft => {
const user = draft.users.get("17")

user.todos = user.todos.filter(todo => todo.done)
})
```

如果你用过immutablejs你会发现immerjs带给你不少便利,因为我们只需要借助produce方法与常用的js api就能达到immutable带给你的效果,大大的降低了学习成本;

----------

**柯里化 producers**

如果你看了以上我在reacthooks当中使用produce的案例你可能会有疑问,为什么在官方案例中produce需要传入baseStete和recipe方法,而我在案例中只传入了recipe方法,这其实是**柯里化 producers**带来的便利;请看以下代码:


```
setList(produce(list,draft => {
draft.push( {
id:3,
value:'烧卖'
})
}))
```


```
setList(produce(draft => {
draft.push( {
id:3,
value:'烧卖'
})
}))
```

以上两段代码得到的结果都是一样的,在我们只传入一个参数并且这个参数是一个方法的时候(recipe方法),produce会返回一个方法,并且当我们往这个返回方法当中再传入一个baseState之后我们依旧会得到一个我们想要的返回值;我们都知道当我们使用useState之后可以用解构的方式给出一个state和一个setState
```
const [state,setState] = useState(10)
```
我们可以在setState方法中通过直接传入我们想要的值

```
setState(state+1)
```
或者通过传入一个回调函数返回一个新值的方式
```
setState(state=>state+1)
```
这两种方式得到的结果都是一样的,但是本质上讲有些许不同(个人建议尽量使用回调函数)immerjs正好利用了setState的这一个特性,当我们只传入recipe方法的时候返回一个新的方法,再通过setState传入的新值进行数据覆盖;

拆分
```
setList(produce(draft => {
draft.push( {
id:3,
value:'烧卖'
})
}))
```
之后其实是这样的


```
setList(list=>{
const getNewState = produce(draft =>{
draft.push({
value:'茶叶蛋',
id:3
})
})
return getNewState(list)
})
```
所以我们可以得出


```
const baseState = [1,2]
produce(baseState,draft=>{
draft.push(3)
})
```
其实等同于

```
const baseState = [1,2]
produce(draft=>{
draft.push(3)
})(baseState)
```
官方是这样给出解释的

**将函数作为第一个参数传递给 produce 会创建一个函数,该函数尚未将 produce 应用于特定 state,而是创建一个函数,该函数将应用于将来传递给它的任何 state。这通常称为柯里化。**


**柯里化 producers**可以让我们在react代码中使用起来更加简便轻巧;让我们看看在react中如何使用immerjs

----------

**React & Immer**

我在前面两节当中已经简单讲述了immerjs如何与useState这个hooks的搭配使用,但是其实immer官方还给我们提供了一个useImmer的hooks,使用方式差不多,但是有少量简化;
要使用useImmer我们需要安装useImmer


```
#npm
npm i use-immer
#yarn
yarn add use-immer
```
当我们安装好之后我们就可以使用useImmer了

首先请看produce与useState的包装案例**useImmer**


```
import react, {useState} from 'react'
import { useImmer } from "use-immer";
export default function IndexPage() {
const [list, setList] = useImmer([
{
id:1,
value:'大饼'
},
{
id:2,
value:'豆浆'
},
]);
const add = ()=>{
setList(draft => {
draft.push({
id:3,
value:'大逼兜'
},)
})
}
return (
<div>
{list.map(item=><div key={item.id}>{item.value}</div>)}
<button onClick={add}>add</button>
</div>
);
}

```
它的实现效果与之前的使用通过setState传入produce一样因为代码比较简单,就不多做解释了;useImmer是基于produce与useState包装之后的产物;

讲完useState我们来讲讲useReducer + Immer如何使用;

首选我们先看下传统的使用useReducer;


```
import react, {useReducer} from 'react'
const baseState = [
{
value:'巴掌',
id:0
},
{
value:'福建烤老鼠',
id:1
},
{
value:'红药水',
id:2
},
]
export default function IndexPage() {
const [list, dispatch] = useReducer((state,action)=>{
switch (action){
case 'dj':
return [...state,{value:'豆浆',id:state.length}]

case 'bz':
return [...state,{value:'包子',id:state.length}]
case 'dbd':
return [...state,{value:'大逼兜',id:state.length}]
}
}, baseState);


return (
<div>
<div>今天早饭吃什么?</div>
{list.map(item=><div key={item.id}>{item.value}</div>)}

<br/>
<br/>


<button onClick={()=>dispatch('dj')}>豆浆</button>
<button onClick={()=>dispatch('bz')}>包子</button>
<button onClick={()=>dispatch('dbd')}>大逼兜</button>
</div>
);
}

```
一个很简单的选择添加案例,每当我们点击按钮调用dispatch方法都会生成一个新的数组去替换原来的数组;
那么我们使用immerjs将如何实现?
首先我们看下produce+useReducer怎么实现;

```
import react, {useReducer} from 'react';
import produce from "immer";
const baseState = [
{
value:'巴掌',
id:0
},
{
value:'福建烤老鼠',
id:1
},
{
value:'红药水',
id:2
},
]
export default function IndexPage() {
const [list, dispatch] = useReducer(
produce((draft,action)=>{
switch (action){
case 'dj':
draft.push({value:'豆浆',id:draft.length})
break;
case 'bz':
draft.push({value:'包子',id:draft.length})
break;
case 'dbd':
draft.push({value:'大逼兜',id:draft.length})
break;
}
}), baseState);


return (
<div>
<div>今天早饭吃什么?</div>
{list.map(item=><div key={item.id}>{item.value}</div>)}

<br/>
<br/>


<button onClick={()=>dispatch('dj')}>豆浆</button>
<button onClick={()=>dispatch('bz')}>包子</button>
<button onClick={()=>dispatch('dbd')}>大逼兜</button>
</div>
);
}

```
可以看到这里我们用了**柯里化 producers**的特性让produce很简便的与useReducer搭配使用,但是其实官方更加简便的useImmerReducer hooks让我们可以更加简便;
请看案例:


```
import react, {useReducer} from 'react';
import {useImmerReducer} from 'use-immer'
const baseState = [
{
value:'巴掌',
id:0
},
{
value:'福建烤老鼠',
id:1
},
{
value:'红药水',
id:2
},
]
export default function IndexPage() {
const [list, dispatch] = useImmerReducer(
(draft,action)=>{
switch (action){
case 'dj':
draft.push({value:'豆浆',id:draft.length})
break;
case 'bz':
draft.push({value:'包子',id:draft.length})
break;
case 'dbd':
draft.push({value:'大逼兜',id:draft.length})
break;
}
}, baseState);


return (
<div>
<div>今天早饭吃什么?</div>
{list.map(item=><div key={item.id}>{item.value}</div>)}

<br/>
<br/>


<button onClick={()=>dispatch('dj')}>豆浆</button>
<button onClick={()=>dispatch('bz')}>包子</button>
<button onClick={()=>dispatch('dbd')}>大逼兜</button>
</div>
);
}

```
与useImmer相同,useImmerReducer也是通过immerjs与reactHooks的封装得到的新的hooks;

**其他**

immerjs在react中常用的方法差不多就这些;最开始我用的是immutablejs+redux来搭建持久化数据层,后来useReducer与useContext的横空出世,我又使用useReducer+useContext+immutablejs搭建持久化数据层,再然后我接触到了immerjs才觉得immutablejs在某些场景下确实繁琐,所以我再加思考之后决定使用immerjs+useReducer+useContext;由于我的习惯是将所有的数据放在数据层统一管理immerjs确实带给我很大的便利;

编辑于 2022-08-31 12:16