React 中使用immer修改state摆脱“不可变”

什么是 Immer

Immer 是一个小型的 JavaScript 库,它让你可以以可变的方式来编写代码,而最终得到的是不可变的数据。想象一下,你可以直接拿起积木修改积木塔,而最后呈现给 React 的是一个全新搭建好的积木塔。这就是 Immer 为我们做的事情。它使用了 Proxy(在现代 JavaScript 环境中)或 ES5 的 Object.defineProperty 来实现这一魔法。

为什么 React 需要不可变状态

在深入了解 Immer 之前,我们先搞清楚为什么 React 要求我们不可变地更新状态。在 React 中,当状态发生变化时,React 会通过比较新旧状态来决定是否重新渲染组件。如果我们直接修改状态,React 可能无法检测到状态的变化,从而导致组件不会重新渲染。而不可变更新会创建一个新的状态对象,React 可以很容易地检测到这个变化并正确地重新渲染组件。

不使用不可变更新的问题示例

下面是一个简单的 React 组件示例,展示了不使用不可变更新可能会遇到的问题:

javascript 复制代码
import React, { useState } from 'react';

// 定义一个简单的 React 组件
const App = () => {
  // 使用 useState 钩子来管理状态,初始状态是一个包含 name 属性的对象
  const [user, setUser] = useState({ name: '张三' });

  // 定义一个函数来更新用户的名字
  const updateName = () => {
    // 直接修改 user 对象,这违反了 React 的不可变更新原则
    user.name = '李四';
    // 尝试设置状态,但 React 不会检测到变化,因为引用没有改变
    setUser(user);
  };

  return (
    <div>
      <p>User name: {user.name}</p>
      <button onClick={updateName}>Update Name</button>
    </div>
  );
};

export default App;

在这个例子中,当我们点击按钮时,user.name 被直接修改了,但 React 不会重新渲染组件,因为 setUser 接收的还是同一个对象引用。

使用不可变更新的正确做法

为了让 React 能够正确检测到状态变化,我们需要使用不可变更新。以下是使用不可变更新的示例:

javascript 复制代码
import React, { useState } from 'react';

// 定义一个简单的 React 组件
const App = () => {
  // 使用 useState 钩子来管理状态,初始状态是一个包含 name 属性的对象
  const [user, setUser] = useState({ name: '张三' });

  // 定义一个函数来更新用户的名字,使用不可变更新
  const updateName = () => {
    // 创建一个新的对象,包含更新后的属性
    const newUser = { ...user, name: '李四' };
    // 设置新的状态
    setUser(newUser);
  };

  return (
    <div>
      <p>User name: {user.name}</p>
      <button onClick={updateName}>Update Name</button>
    </div>
  );
};

export default App;

在这个例子中,我们使用展开运算符 ... 创建了一个新的对象 newUser,并将更新后的 name 属性赋值给它。然后将 newUser 传递给 setUser,这样 React 就能检测到状态的变化并重新渲染组件。

Immer使用

安装 Immer

在开始使用 Immer 之前,我们需要先安装它。可以使用 npm 或 yarn 来安装:

javascript 复制代码
npm install immer
# 或者
yarn add immer
Immer 的基本使用方法
创建一个 draft

Immer 的核心概念是 draft(草稿)。当你使用 Immer 时,它会给你一个可以直接修改的 draft 对象,而不是原始的状态对象。在你完成对 draft 的修改后,Immer 会基于这些修改创建一个新的不可变状态对象。

下面是一个使用 Immer 的简单示例:
javascript 复制代码
import { useState } from "react";
import {produce} from 'immer';
function App() {
  // 使用 useState 钩子来管理状态,初始状态是一个包含 name 属性的对象
 const [user, setUser] = useState({ name: '张三' });
 // 定义一个函数来更新用户的名字,使用 Immer 的 produce 函数
  const updateName = () => {
    // 使用 produce 函数创建一个 draft 对象
    const newUser = produce(draft => {
      // 直接修改 draft 对象,就像修改普通对象一样
      draft.name = '李四';
    });
    // 设置新的状态
    setUser(newUser);
  };
  return (
    <>
     <p>User name: {user.name}</p>
      <button onClick={updateName}>Update Name</button>
    </>
  );
}

export default App;

在这个例子中,produce 函数可以接收两个参数:原始状态 user 和一个回调函数。回调函数接收一个 draft 对象,我们可以直接在这个 draft 对象上进行修改。当回调函数执行完毕后,produce 会返回一个新的不可变状态对象 newUser,我们将这个新对象传递给 setUser 来更新状态。

处理数组状态

Immer 特别擅长处理嵌套状态。在 React 应用中,状态往往是嵌套的对象或数组,手动进行不可变更新会变得非常复杂。而使用 Immer,我们可以像处理普通对象一样轻松地修改嵌套状态。

数组对象示例

下面是一个处理嵌套对象状态的示例:

javascript 复制代码
import { useState } from "react";
import { produce } from "immer";
const List = () => {
  const [list, setList] = useState([
    { id: "1", title: "问卷一", isPublished: false },
    { id: "2", title: "问卷二", isPublished: true },
    { id: "3", title: "问卷三", isPublished: false },
    { id: "4", title: "问卷四", isPublished: true },
  ]);
  const deleteEdit = (id: string) => {
    //使用immer
    const newList = produce((draft) => {
      return draft.filter(item => item.id != id);
    });
    setList(newList);

    //不可变更新
    //  const list1 = list.filter(item=>item.id !=id)
    // setList(list1);
  };
  const addEdit = () => {
    //使用immer
    const newList = produce((draft) => {
      draft.push({
        id: new Date().toString(),
        title: "新闻卷" + new Date(),
        isPublished: false,
      });
    });
    setList(newList);
    //不可变更新
    //  setList(
    //   list.concat({
    //     id: new Date().toString(),
    //     title: "问卷五",
    //     isPublished: false,
    //   })
    // );
  };
  return (
    <>
      <h1>问卷调查</h1>
      <div>
        <div>
          <button onClick={addEdit}>新增问卷</button>
        </div>
        {list.map((item) => {
          const { id, title, isPublished } = item;
          return (
            <div key={id}>
              <strong>{title}</strong>
              &nbsp; &nbsp;
              {isPublished ? (
                <span style={{ color: "green" }}>已发布</span>
              ) : (
                <span>未发布</span>
              )}
              &nbsp; &nbsp;
              <button onClick={() => deleteEdit(id)}>删除问卷</button>
            </div>
          );
        })}
      </div>
    </>
  );
};
export default List;

在这个例子中,我们有一个嵌套的数组 state,包含多个对象使用 Immer,我们可以直接在 draft 对象上修改嵌套数组中的元素,而不需要手动创建新的数组和对象。

在 Redux 中使用 Immer

Immer 也可以和 Redux 一起使用,让 Redux 的 reducer 函数更加简洁和易于维护。在 Redux 中,reducer 函数必须返回一个新的状态对象,而不能直接修改原始状态。使用 Immer,我们可以在 reducer 函数中以可变的方式编写代码。

Redux 示例
javascript 复制代码
import { createStore } from 'redux';
import produce from 'immer';

// 定义初始状态
const initialState = {
  users: [
    { id: 1, name: 'John', age: 25 },
    { id: 2, name: 'Jane', age: 30 }
  ],
  selectedUser: null
};

// 定义 reducer 函数,使用 Immer 的 produce 函数
const reducer = produce((draft, action) => {
  switch (action.type) {
    case 'ADD_USER':
      // 直接在 draft 对象的 users 数组中添加一个新用户
      draft.users.push(action.payload);
      break;
    case 'UPDATE_USER':
      // 找到要更新的用户
      const userToUpdate = draft.users.find(user => user.id === action.payload.id);
      if (userToUpdate) {
        // 直接修改用户的属性
        Object.assign(userToUpdate, action.payload);
      }
      break;
    case 'SELECT_USER':
      // 直接设置 selectedUser 属性
      draft.selectedUser = action.payload;
      break;
    default:
      break;
  }
}, initialState);

// 创建 Redux 商店
const store = createStore(reducer);

// 订阅商店的变化
store.subscribe(() => {
  console.log('Current state:', store.getState());
});

// 分发一个 ADD_USER 动作
store.dispatch({
  type: 'ADD_USER',
  payload: { id: 3, name: 'Bob', age: 35 }
});

// 分发一个 UPDATE_USER 动作
store.dispatch({
  type: 'UPDATE_USER',
  payload: { id: 1, name: 'Updated John', age: 26 }
});

// 分发一个 SELECT_USER 动作
store.dispatch({
  type: 'SELECT_USER',
  payload: { id: 2, name: 'Jane', age: 30 }
});

在这个例子中,我们有一个包含多个用户的状态对象,以及一个 selectedUser 属性。使用 Immer,我们可以在 reducer 函数中直接修改 draft 对象,而不需要手动创建新的对象和数组。

Immer 的优势

代码简洁性

使用 Immer 可以让我们的代码更加简洁和易于理解。在不使用 Immer 的情况下,我们需要手动进行不可变更新,这会导致代码变得冗长和复杂。而使用 Immer,我们可以直接在 draft 对象上进行修改,就像修改普通对象一样,代码变得更加直观。

减少错误

手动进行不可变更新容易出错,特别是在处理嵌套状态时。我们可能会忘记创建新的对象或数组,从而导致状态更新失败。而使用 Immer,我们可以避免这些错误,因为 Immer 会自动处理不可变更新。

提高开发效率

由于 Immer 让我们可以以可变的方式编写代码,我们可以更快地实现状态更新逻辑。我们不需要花费大量的时间来手动创建新的对象和数组,从而提高了开发效率。

更好的代码可维护性

使用 Immer 编写的代码更加易于维护。当我们需要修改状态更新逻辑时,我们只需要在 draft 对象上进行修改,而不需要担心不可变更新的细节。这使得代码的可读性和可维护性都得到了提高。

总结

总结:Immer 是一个非常强大的库,它让我们可以以可变的方式编写代码,而最终得到的是不可变的数据。在 React 中使用 Immer 可以简化状态更新逻辑,提高代码的简洁性、减少错误、提高开发效率和代码的可维护性。无论是处理简单的状态还是复杂的嵌套状态,Immer 都能帮助我们轻松应对

相关推荐
xw51 小时前
Trae安装指定版本的插件
前端·trae
默默地离开1 小时前
前端开发中的 Mock 实践与接口联调技巧
前端·后端·设计模式
南岸月明1 小时前
做副业,稳住心态,不靠鸡汤!我的实操经验之路
前端
嘗_1 小时前
暑期前端训练day7——有关vue-diff算法的思考
前端·vue.js·算法
伍哥的传说1 小时前
React 英语打地鼠游戏——一个寓教于乐的英语学习游戏
学习·react.js·游戏
MediaTea2 小时前
Python 库手册:html.parser HTML 解析模块
开发语言·前端·python·html
杨荧2 小时前
基于爬虫技术的电影数据可视化系统 Python+Django+Vue.js
开发语言·前端·vue.js·后端·爬虫·python·信息可视化
BD_Marathon2 小时前
IDEA中创建Maven Web项目
前端·maven·intellij-idea
waillyer2 小时前
taro跳转路由取值
前端·javascript·taro
凌辰揽月2 小时前
贴吧项目总结二
java·前端·css·css3·web