Redux 中更改复杂 state 的好办法—Immer

Redux 中更改复杂 state 的好办法—Immer.png

目录

  • 遇到的问题
  • Immer 基本使用
  • 高阶 produce
  • use-immer
  • 总结

遇到的问题

React 项目中,无论是在组件中还是在 Redux 中我们去改变 state,都必须返回一个副本 state,而不能在原有的 state 中直接更改。

当数据量非常小的时候没有太大的问题,但是当我们遇到一个 state 是超大的对象或者数组问题就显现出来了,比如我们去更新 lat 字段:

const [state, setState] = useState({
    "id": 1,
    "name": "Leanne Graham",
    "username": "Bret",
    "email": "[email protected]",
    "address": {
        "street": "Kulas Light",
        "suite": "Apt. 556",
        "city": "Gwenborough",
        "zipcode": "92998-3874",
        "geo": {
            "lat": "-37.3159",
            "lng": "81.1496"
        }
    },
    "phone": "1-770-736-8031 x56442",
    "website": "hildegard.org",
    "company": {
        "name": "Romaguera-Crona",
        "catchPhrase": "Multi-layered client-server neural-net",
        "bs": "harness real-time e-markets"
    }
});

setState 的写法就会是这样:

setState({
    ...state,
    address: {
        ...state.address,
        geo: {
            ...state.address.geo,
            lat: "88.8888"
        }
    }
})

这种写法就非常的糟心了,我只需要更新一个字段,却需要三次强制解构,这种情况,只要一不小心就会出现问题,真心讲,我项目中有一个确实因为这个问题出现过 bug 。

Redux 为什么需要返回一个新的 state?
因为在 Redux 中,状态被视为不可变的,永远不应该直接修改(这是因为 UI 需要更新会对比简单暴力对比 state 的状态,所以无论是对象还是数组你直接修改数据,引用地址是不变的,必须返回一个新的),而是通过复制现有的对象/数组,然后修改副本。

好了,我们找到了痛点,怎么去解决呢,接下里就有请今日主角出场。

现在最好的一个解决方案就是使用——Immer 采用数据的双向绑定,更改数据让原数据自动改变。

Immer 的核心实现就是 Vue 的双向绑定原理,优先会使用 proxy,如果不支持会降级到 Object.definedProperty 来实现。

不过,话说回来,在 React 中去支持双向绑定,难道直接用 Vue 不香吗。

Immer 基本使用

我们知道 React 周边的技术栈,都很难学,主要是概念一大推,不过不用担心 Immer 难学,因为它的核心 API 只有一个 就是 produce。

首先项目中安装 Immer:

npm install -D immer

接下来,我们看看如何把上面那个糟心的案例给修改成功:

import { produce } from "immer";

const state = {
    "id": 1,
    "name": "Leanne Graham",
    "username": "Bret",
    "email": "[email protected]",
    "address": {
        "street": "Kulas Light",
        "suite": "Apt. 556",
        "city": "Gwenborough",
        "zipcode": "92998-3874",
        "geo": {
            "lat": "-37.3159",
            "lng": "81.1496"
        }
    },
    "phone": "1-770-736-8031 x56442",
    "website": "hildegard.org",
    "company": {
        "name": "Romaguera-Crona",
        "catchPhrase": "Multi-layered client-server neural-net",
        "bs": "harness real-time e-markets"
    }
};

const nextState = produce(state, (draftState) => {
    draftState.address.geo.lat = "88.8888";
});


setState(nextState);

这就很简单了,我们通过 produce 函数传入需要更改的 state,然后在回调参数里面获取被双向绑定的 state,更改完成之后,返回已经被更改的 state。

过程就是辣么简单,但是到 Immer 这里描述可就不是那么的简单了,双向绑定之后的 state Immer 给出了一个新概念 draft,下面是一张显示 Immer 工作原理的图片(取自官方文档):

Immer 原理

Immer 提供了一个辅助函数,它将一个状态作为参数并生成一个可以直接修改的草稿状态,然后根据所有应用的更改创建一个新的状态对象。

高阶 produce

上面的 produce 案例,有点太 low,高级点我们可以稍微进行封装下,传入要修改的 key 和要修改的 value。

提起代码如下:

const changeGeoLat = (key, value) => {
    return produce(state, (draftState) => {
        draftState.address.geo[key] = value;
    })
};

setState(changeGeoLat("lat", "88.8888"));

这种情况下,produce 提供了一种更加便利的操作:

const changeGeoLat = produce((draftState, key, value) => {
    draftState.address.geo[key] = value;
});

console.log(changeGeoLat(state, "lat", "88.8888"));

省略了一层函数,其实并没有省略,我们从源码层面看下文件路径:node_modules/immer/src/core/immerClass.ts


export class Immer implements ProducersFns {

    produce: IProduce = (base: any, recipe?: any, patchListener?: any) => {
        // curried invocation
        if (typeof base === "function" && typeof recipe !== "function") {
            const defaultBase = recipe
            recipe = base

            const self = this
            return function curriedProduce(
                this: any,
                base = defaultBase,
                ...args: any[]
            ) {
                return self.produce(base, (draft: Drafted) => recipe.call(this, draft, ...args)) // prettier-ignore
            }
        }
    }
}

Immer 会判断你传入的第一个参数 base 是不是函数,如果是,函数 produce 定义了一个高阶函数,会把通过 call 自动调用 recipe。

好了,了解到这完全可以了,可能这个 Curried producers 的概念比较难, 但是 Immer 的基础核心 API produce 还是比较简单的。

use-immer

Immer 的思想是好的,但是在 hook 中这样使用过于麻烦毕竟每次改变都需要 去调用 produce。

其实不难想到,最方便使用 Immer 就是下面这样:

const [count, setCount] = produce(10)

setCount(count++);

把 useState 的逻辑封装到 produce 中,通过 produce 返回的状态直接就是双向绑定的。

这解释今天的第二主角 use-immer。


import React from "react";
import { useImmer } from "use-immer";


function App() {
    const [person, updatePerson] = useImmer({
        name: "Michel",
        age: 33
    });

    function updateName(name) {
        updatePerson(draft => {
            draft.name = name;
        });
    }

    function becomeOlder() {
        updatePerson(draft => {
            draft.age++;
        });
    }

    return (
        

Hello {person.name} ({person.age})

{ updateName(e.target.value); }} value={person.name} />
); }

另外,如果是数字、字符串、布尔值为了方便可以不使用回调操作,就是下面这样:

const [count, setCount] = useImmer(0);

const incrementFatherAge = () => {
  setCount(count + 1);
};

use-immer 还提供另外一个 API useImmerReducer,这个留给大家探索吧。

总结

今天我们学习了,在使用 React 维护状态的过程中,当遇到嵌套过深的数组或对象时,每次更改都要小心意义。

于是我们找到了 Immer 这个库,用双向绑定来解决必须返回一个副本,为了能够更好的和 Hook 结合,我们又学习了use-immer。

最后,如果您在使用 React,而还没有使用过它们,我强烈建议您仔细阅读文档并立即开始用起来。

用着用着你就会发现还是他娘的 Vue 好用,哈哈哈哈。

今天国庆了,大家是去旅游了,还是回家收玉米了呢?不过今天我要去看个电影「长津湖」,票价有点贵,但是重工业电影还是大屏看着爽,走起,大家国庆快乐!。

你可能感兴趣的:(Redux 中更改复杂 state 的好办法—Immer)