当前位置: 首页 > 工具软件 > immer > 使用案例 >

使用 Immer 代替 immutable 在 react 项目中实现不可变数据

慕俊语
2023-12-01

说明

immer 的主旨是通过更简单的方式使用 immutable 不可变数据,它是基于 copy-on-write 机制的(如果资源重复未被修改,则无需创建新的资源,资源会在副本与原始位置之间共享;修改则必须创建一个副本;通过这个机制 复制操作会被推迟到第一次写入时进行,显著的减少资源消耗,同时资源修改操作增加少量开销)

  • 通常来讲 react 的 state 以及 redux 存放在 store 中的 state 都是不允许直接修改的, 需要我们手动去创建一个新的 state
  • 如果我们存放的数据中有 Array 或者 Object 类型的就会比较麻烦,需要一层层解构
  • 之前用的 immutable.js 是通过创造了新的数据类型 ImmutableMap、ImmutableSet 等,来提供方便的 api 进行新数据的创建
  • Immer, 与 immutable.js 不同,他没有创建新数据类型,而是在我们改动 state 的操作时,提供一个 proxy 草稿对象,针对草稿对象,我们可以直接修改它的值,而不需要层层解构,Immer 会帮我们把修改后的 草稿对象转变为一个新的普通对象
  • 也是就说使用 Immer 除了在改变 state 是需要介入,其他开发过程中我们与它的接触不多,操作的还是我们熟悉的普通JS内置对象

Immer 的 Api

immer 中有如下三种数据概念

  • currentState: 原始数据
  • draftState: 草稿数据,是一个以原始数据为基础的 Proxy , 数据的操作会在 draftState 上进行
  • nextState: 一旦在 draftState 上的数据操作完成,immer 就会生成 nextState, 原始数据 currentState 将不会被改变

1. produce(baseState, draftState => {}) 用于包装数据处理,并生产新数据

  const baseState = {
    obj: { b: 'xiaobai ' },
    arr: [{ name: 'xiaobai', age: 20 }, { name: '小黑' }],
  };

  const nextState = produce(baseState, (draftState) => {
    // 直接操作 draftState 即可,不需要返回值
    draftState.arr.push({ name: 'ak' });
    console.log(draftState); // Proxy { <target>: {…}, <handler>: {…} }
  });

  console.log(
    nextState, // 返回的是普通对象
    baseState,
    nextState === baseState, // false 因为有值改变
    nextState.obj === baseState.obj, // true 因为 baseState.obj 没有改变
    nextState.arr === baseState.arr, // false
  );

2. produce(draftState => {})(baseState) 用于创建一个 producer(预绑定生产者)

预绑定生产者 producer 可以接收一个 baseState 参数来生成 nextState

  const baseState = {
    obj: { b: 'xiaobai ' },
    arr: [{ name: 'xiaobai', age: 20 }, { name: '小黑' }],
  };

  const baseState2 = {
    obj: { b: 'bbb' },
    arr: [{ name: 'xiaobai', age: 20 }, { name: '小白' }],
  };

  const producer = produce((draftState: typeof baseState) => {
    draftState.arr.push({ name: 'ak' });
  });

  const nextState = producer(baseState);
  const nextState2 = producer(baseState2);

  console.log(nextState, nextState2);

3. produce((draftState, ...args) => {}, initialState)(baseState, ...args) producer 可以接收更多的参数, 以及初始数据

  const baseState = {
    obj: { b: 'xiaobai ' },
    arr: [{ name: 'xiaobai', age: 20 }, { name: '小黑' }],
  };

  const baseState2 = {
    obj: { b: 'bbb' },
    arr: [{ name: 'xiaobai', age: 20 }, { name: '小白' }],
  };

  const producer = produce(
    (draftState: typeof baseState, data?: { name: string }) => {
      if (data) draftState.arr.push(data);
    },
    { obj: { b: '' }, arr: [] }, // initialState
  );

  const nextState = producer(); // 不传返回初始值
  const nextState2 = producer(baseState2, { name: 'second' });

  console.log(nextState, nextState2);

4. immer.current immer.original 用于在 produce 中提取 draft 的当前值或者原始传入值 (注意:操作昂贵,一般只用于测试)

const base = {
    x: 0
}

const next = produce(base, draft => {
    draft.x++
    const orig = original(draft)
    const copy = current(draft)
    console.log(orig.x)
    console.log(copy.x)

    setTimeout(() => {
        // this will execute after the produce has finised!
        console.log(orig.x)
        console.log(copy.x)
    }, 100)

    draft.x++
    console.log(draft.x)
})
console.log(next.x)

// This will print
// 0 (orig.x)
// 1 (copy.x)
// 2 (draft.x)
// 2 (next.x)
// 0 (after timeout, orig.x)
// 1 (after timeout, copy.x)

Immer 与 React、Redux 的结合使用

1. React setState 中怎么使用 immer

  • setState 可以传一个函数,setState((prevState) => nextState);
  • producer 的刚好是一个接收 原始数据,输出新数据的函数
  • 我们可以将 producer 传入 setState
  // 不使用 immer 
  const { remote } = this.state;
  const { cache, data } = remote;
  // 需要一层层解构
  this.setState({ remote: { ...remote, cache: [...cache, data] } });

  // 使用 immer 
  this.setState(
    // import {Draft} from 'immer'
    produce((state: Draft<State>) => {
      // 直接操作数据即可
      state.remote.cache.push(state.remote.data);
    }),
  );
  // 或者
  this.setState(
    produce<State>((state) => {
      // 直接操作数据即可
      state.remote.cache.push(state.remote.data);
    }),
  );

2. redux 的 reducer 中如何使用 immer

// 不使用 immer
function counterReducer(state = { counter: 0 }, action: { type: string }) {
  switch (action.type) {
    case 'counter/incremented':
    // 需要 return 一个新的 state 
      return { counter: state.counter + 1 };
    case 'counter/decremented':
      return { counter: state.counter - 1 };
    default:
      return state;
  }
}

// 使用 immer 后

const INITIAL_DATA = { counter: 0 };

const counterReducer = produce(
  (state: typeof INITIAL_DATA, action: { type: string }) => {
    switch (action.type) {
      case 'counter/incremented':
        state.counter += 1;
        break;
      case 'counter/decremented':
        state.counter -= 1;
        break;
      default:
    }
  },
  INITIAL_DATA,
);

一般来讲使用 immer 的 reducer 不需要 return 任何内容,如果一定要 return 可以查看 Returning new data from producers

需要特殊说明的是,如果需要 return 一个 undefined 该怎么操作?,直接 return undefined 是不行的,者会被认为是 return draft,需要按照如下操作

import produce, {nothing} from "immer"

const state = {
    hello: "world"
}

produce(state, draft => nothing) // 相当于将 state 置为 undefined

其他

  • Redux 的版本到 4.x.x 以后以及趋于稳定,变更越来越少,但是基础版的 Api 使用起来比较繁琐,所以 Redux 团队又推出了一个封装版 Redux Toolkit , 这个封装版,是 redux 与 immer 的结合体。

如果想要使用 封装版请查看 Redux Toolkit

 类似资料: