使用 Immer

Flying
2022-01-05 / 0 评论 / 129 阅读 / 正在检测是否收录...

Immer 是一个 JavaScript 库,用于简化处理不可变数据的操作。它的目标是使数据的修改过程更加直观和易于理解,同时保持不可变性的特性。传统上,处理不可变数据需要进行大量的手动操作,例如深层复制对象、创建新的数组副本等。这样的操作不仅繁琐,而且容易出错。Immer 的出现解决了这个问题。

imme.svg

React 的设计理念鼓励使用不可变数据来提高性能和可维护性,因为它可以帮助我们更好地追踪数据的变化,进行状态比较,并准确地确定需要重新渲染的组件。在 React 项目中,如果要更改的组件不是仅有一个基本类型的属性值,则需要深层复制状态对象,然后与新属性值进行合并,不能直接更改原有状态。

问题

当数据层级不深时没有太大的问题,但是当我们遇到一个 state 很复杂的对象或者数组问题就显现出来了,比如我们要更新 lat 字段:

const [state, setState] = useState({
  id: 1,
  title: '豪方花园'
  city: '深圳市',
  address: {
    street: '南山区南海大道 4040 号',
    zipcode: '518000',
    geo: {
      lat: 22.548669,
      lng: 113.941641
    }
  }
})

setState 的写法就会是这样:

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

这种写法就非常的啰嗦了,只需要更新一个字段,却需要三次展开复制合并数据,一不留神就出 bug 了。在无法修改数据结构的情况下咋解决这个问题呢?我知道的最好办法就是使用 Immer。

Immer

Immer 是一个用于处理不可变数据的 JavaScript 库,它通过引入一个简单的可变草稿(draft)概念,使得在不直接修改原始数据的情况下进行更新变得更加容易和直观。基本原理如下:

immer.webp
Immer 原理(来源官方文档)

  1. 创建草稿(Draft):使用 Immer 的 produce 函数,你可以传入一个原始数据对象以及一个函数,该函数描述了如何修改数据。Immer 将根据这个函数创建一个草稿副本。
  2. 在草稿上进行修改:在草稿中,你可以像在常规 JavaScript 对象上一样直接修改数据,而不用担心不可变性。Immer 会记录每个修改操作。
  3. 生成新的不可变状态:当你完成对草稿的修改时,你可以从草稿中生成一个新的不可变状态。Immer 会比较原始数据和草稿之间的差异,并生成一个全新的不可变状态对象。
  4. 避免不必要的修改:Immer 通过结构共享和惰性复制等技术,确保只有真正发生变化的数据才会被复制。这样可以减少不必要的内存开销。

使用 Immer 非常简单,首先项目中安装 Immer:

npm install -D immer

以下是使用 Immer 的基本示例:

import produce from 'immer'

const state = {
  count: 0,
  todos: []
}

const newState = produce((draft) => {
  draft.count += 1
  draft.todos.push('New todo')
})

在上面的例子中,我们使用 produce 函数创建了一个新的状态 newState。在草稿函数中,我们可以直接对 draft 进行修改,而不需要担心不可变性。Immer 会自动处理这些修改并生成一个新的不可变状态。

接下来,我们使用 Immer,将前面更新经度的案例修改一下:

import { useState } from 'react'
import { produce } from 'immer'

const Map = () => {
  const [state, setState] = useState({
    id: 1,
    title: '豪方花园',
    city: '深圳市',
    address: {
      street: '南山区南海大道 4040 号',
      zipcode: '518000',
      geo: {
        lat: 22.548669,
        lng: 113.941641
      }
    }
  })

  function handleUpdate() {
    setState(
      produce((draft) => {
        draft.address.geo.lat = 21.125
      })
    )
  }

  return (
    <>
      <div>{state.address.geo.lat}</div>
      <button onClick={handleUpdate}>Update</button>
    </>
  )
}

export default Map

由于所有 state 的更新都使用 produce 包装的更新模式,所以我们可以通过将更新模式包装在 use-immer 包中来简化上述操作

useImmer Hook

useImmer 是一个自定义 React Hook,它基于 Immer 库,用于在函数组件中管理可变状态。useImmer 简化了使用 Immer 的操作过程,并提供了一种方便的方式来更新状态并触发组件重新渲染。

useImmer 的基本原理如下:

  1. 在组件内部使用 useImmer Hook:在函数组件中使用 useImmer Hook,类似于使用其他 React Hook。它接受一个初始状态(可以是一个值或一个函数),并返回一个包含状态值和状态更新函数的数组。
  2. 创建状态和更新函数:在 useImmer 内部,它会使用 useState 来创建一个状态值(称为 state )和一个更新函数(称为 setState)。初始状态会被传递给 useState,并返回一个包含状态和更新函数的数组。
  3. 使用 Immer 创建草稿:useImmer 在内部使用 Immer 的 produce 函数创建一个草稿副本。这个草稿副本会与初始状态相对应,并用于记录状态的修改。
  4. 状态更新函数:setState 函数被返回给组件,供组件在需要更新状态时调用。当调用 setState 时,它会接收一个更新函数作为参数,该函数描述了如何修改状态。在更新函数内部,你可以直接修改草稿,就像在常规 JavaScript 对象上一样。
  5. 生成新的状态和触发重新渲染:当在更新函数中完成对草稿的修改时,Immer 会根据草稿和初始状态之间的差异生成一个全新的不可变状态。然后,useImmer 将更新组件的内部状态,并触发组件的重新渲染。

这样,每当状态更新函数被调用时,useImmer 会处理 Immer 相关的逻辑,使得在组件中处理可变状态变得更加简洁和直观。

以下是一个使用 useImmer 的简单示例:

import { useImmer } from 'use-immer'

function Counter() {
  const [state, setState] = useImmer({ count: 0 })

  const increment = () => {
    setState((draft) => {
      draft.count += 1
    })
  }

  // ...
}

在上面的例子中,我们使用 useImmer 创建了一个可变状态 state 和一个状态更新函数 setState。在 increment 函数中,我们使用 setState 来更新状态。在更新函数内部,我们可以直接修改草稿对象 draft 的属性,而无需担心不可变性。useImmer 会自动处理状态的更新和重新渲染。

正如你看到的,useImmer 就是对 useState Hook 和 produce 函数的封装,在 useImmer 中,你甚至看不到 produce 函数的影子。

接下来,我们使用 Immer,将前面更新经度的案例重写一下:

import { useImmer } from 'use-immer'
import { useImmer } from 'use-immer'

const Map = () => {
  const [state, setState] = useImmer({
    id: 1,
    title: '豪方花园',
    city: '深圳市',
    address: {
      street: '南山区南海大道 4040 号',
      zipcode: '518000',
      geo: {
        lat: 22.548669,
        lng: 113.941641
      }
    }
  })

  function handleUpdate() {
    setState((draft) => {
      draft.address.geo.lat = 21.125
    })
  }
  // ...
}
// ...

数组操作

React 中对数组增删改操作如果不使用 Immer,对新手来说不是那么好理解:

function handleAddTodo(title) {
  setTodos([
    ...todos,
    {
      id: nextId++,
      title: title,
      done: false
    }
  ])
}

function handleChangeTodo(nextTodo) {
  setTodos(
    todos.map((t) =>
      t.id === nextTodo.id
        ? {
            ...t,
            title: nextTodo.title,
            done: nextTodo.done
          }
        : t
    )
  )
}

function handleDeleteTodo(todoId) {
  setTodos(todos.filter((t) => t.id !== todoId))
}

使用 Immer 或 userImmer 代码就清晰多了:

function handleAddTodo(title) {
  setTodos((draft) => {
    draft.push({
      id: nextId++,
      title: title,
      done: false
    });
  });
}

function handleChangeTodo(nextTodo) {
  setTodos((draft) => {
    const todo = draft.find((t) => t.id === nextTodo.id) as Todo;
    todo.title = nextTodo.title;
    todo.done = nextTodo.done;
  });
}

function handleDeleteTodo(todoId) {
  setTodos((draft) => {
    const index = draft.findIndex((t) => t.id === todoId);
    draft.splice(index, 1);
  });
}

毕竟更改可变数据比更改不可变数据好理解很多,操作复杂数据也方便很多。Redux Toolkit 内置了 Immer,可见其重要性。如果你使用 Vue.js,就没必要使用 Immer 了,因为 Vue.js 已经实现了可变数据的响应式状态。

总结

使用 Immer 可以简化 React 应用中的状态管理,使得对不可变数据的更新更加直观和容易,同时减少不必要的内存开销。当然,Immer 不局限于只能在 React 应用中使用,需要简化不可变数据操作的地方都可以使用它。

链接

Immer官方文档

1

评论 (0)

取消