immerjs在react中的使用

662 阅读9分钟

我们都知道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确