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 都能帮助我们轻松应对

相关推荐
fruge15 小时前
前端正则表达式实战合集:表单验证与字符串处理高频场景
前端·正则表达式
baozj15 小时前
🚀 手动改 500 个文件?不存在的!我用 AST 撸了个 Vue 国际化神器
前端·javascript·vue.js
用户40993225021216 小时前
为什么Vue 3的计算属性能解决模板臃肿、性能优化和双向同步三大痛点?
前端·ai编程·trae
海云前端116 小时前
Vue首屏加速秘籍 组件按需加载真能省一半时间
前端
蛋仔聊测试16 小时前
Playwright 中route 方法模拟测试数据(Mocking)详解
前端·python·测试
零号机16 小时前
使用TRAE 30分钟极速开发一款划词中英互译浏览器插件
前端·人工智能
疯狂踩坑人16 小时前
结合400行mini-react代码,图文解说React原理
前端·react.js·面试
Mintopia16 小时前
🚀 共绩算力:3分钟拥有自己的文生图AI服务-容器化部署 StableDiffusion1.5-WebUI 应用
前端·人工智能·aigc
街尾杂货店&16 小时前
CSS - transition 过渡属性及使用方法(示例代码)
前端·css
CH_X_M17 小时前
为什么在AI对话中选择用sse而不是web socket?
前端