Immer 实战案例解析:让不可变数据操作更简单

在 React、Redux 等前端框架的开发中,"状态不可变"是核心原则------直接修改原始状态会导致组件无法正常重渲染、状态追踪失效等问题。但原生 JavaScript 操作嵌套层级较深的不可变数据时,需要逐层拷贝,代码冗余且易出错。

Immer 作为一款轻量 JavaScript 库,恰好解决了这一痛点。它的核心 API produce 允许我们用"可变的写法"修改"草稿对象(draft)",底层会自动追踪变化、生成全新的不可变数据,实现"写时可变,读时不可变"。本文将通过四大高频实战案例,带你吃透 Immer 的用法,感受其核心优势。

一、核心前提:Immer 核心 API 简介

Immer 的核心能力集中在 produce 方法上,语法简洁且灵活,主要有两种常用形式:

ini 复制代码
import { produce } from "immer";

// 形式1:直接传入原始数据和修改函数
const newData = produce(originalData, draft => {
  // 直接修改 draft(草稿),无需手动拷贝
  draft.xxx = 新值;
});

// 形式2:柯里化(先定义修改逻辑,后续传入原始数据)
const updateFn = produce((draft, payload) => {
  draft.xxx = payload; // 可接收额外参数
});
const newData = updateFn(originalData, 新值);

关键说明:originalData 是原始不可变数据,draft 是 Immer 生成的代理对象(可直接修改),newData 是最终生成的全新不可变数据,原始数据始终保持不变。

二、实战案例:Immer 核心用法落地

以下案例均采用"原生写法 vs Immer 写法"对比形式,直观体现 Immer 简化代码、降低心智负担的优势。

案例1:修改嵌套对象(高频场景)

场景:有一个多层嵌套的用户信息对象,需要修改深层的"年龄"字段,同时修改外层的"姓名"字段。

原生写法(繁琐且易出错)

原生操作需逐层用扩展运算符(...)拷贝,嵌套越深,拷贝层级越多,代码冗余度呈指数级增加:

arduino 复制代码
// 原始数据
const oldState = {
  user: {
    name: "张三",
    info: { age: 20, address: "北京", contact: { phone: "13800138000" } }
  }
};

// 修改:姓名改为"李四",年龄改为21
const newState = {
  ...oldState, // 拷贝最外层对象
  user: {
    ...oldState.user, // 拷贝 user 层
    name: "李四", // 修改姓名
    info: {
      ...oldState.user.info, // 拷贝 info 层
      age: 21, // 修改年龄
      contact: {
        ...oldState.user.info.contact // 若需修改 phone,需再拷贝 contact 层
      }
    }
  }
};

console.log(oldState.user.info.age); // 20(原始数据不变)
console.log(newState.user.info.age); // 21(新数据生效)

Immer 写法(简洁直观)

无需手动逐层拷贝,直接定位到目标字段修改即可,代码量大幅减少,可读性提升:

ini 复制代码
import { produce } from "immer";

// 原始数据(同原生案例)
const oldState = {
  user: {
    name: "张三",
    info: { age: 20, address: "北京", contact: { phone: "13800138000" } }
  }
};

// Immer 修改逻辑
const newState = produce(oldState, draft => {
  draft.user.name = "李四"; // 直接修改外层字段
  draft.user.info.age = 21; // 直接修改深层字段
  // 若需修改 phone,直接写 draft.user.info.contact.phone = "13900139000" 即可
});

console.log(oldState.user.info.age); // 20(原始数据不变)
console.log(newState.user.info.age); // 21(新数据生效)

案例2:操作数组(增删改查)

场景:有一个任务列表数组,需要完成"标记指定任务为完成""新增任务""删除任务"三个操作。

原生写法(需手动生成新数组)

数组的 pushsplice 等方法会修改原数组,因此需用 mapfilter 等方法生成新数组,操作繁琐:

ini 复制代码
// 原始任务列表
const oldList = [
  { id: 1, title: "学习 Immer", done: false },
  { id: 2, title: "写实战案例", done: false },
  { id: 3, title: "整理笔记", done: false }
];

// 需求1:标记 id=2 的任务为完成
// 需求2:新增 id=4 的任务
// 需求3:删除 id=3 的任务
const newList = oldList
  .map(item => item.id === 2 ? { ...item, done: true } : item) // 标记完成(生成新对象)
  .concat({ id: 4, title: "分享文章", done: false }) // 新增任务(生成新数组)
  .filter(item => item.id !== 3); // 删除任务(生成新数组)

console.log(oldList.length); // 3(原始数组不变)
console.log(newList.length); // 3(新数组:id=1、2、4)

Immer 写法(直接操作数组)

可直接使用 pushsplice 等"可变"方法操作 draft 数组,Immer 会自动转换为不可变操作:

ini 复制代码
import { produce } from "immer";

// 原始任务列表(同原生案例)
const oldList = [
  { id: 1, title: "学习 Immer", done: false },
  { id: 2, title: "写实战案例", done: false },
  { id: 3, title: "整理笔记", done: false }
];

// Immer 操作数组
const newList = produce(oldList, draft => {
  // 1. 标记 id=2 的任务为完成
  const targetItem = draft.find(item => item.id === 2);
  if (targetItem) targetItem.done = true;
  
  // 2. 新增任务(直接 push)
  draft.push({ id: 4, title: "分享文章", done: false });
  
  // 3. 删除 id=3 的任务(直接 splice)
  const deleteIndex = draft.findIndex(item => item.id === 3);
  if (deleteIndex !== -1) draft.splice(deleteIndex, 1);
});

console.log(oldList.length); // 3(原始数组不变)
console.log(newList.length); // 3(新数组:id=1、2、4)

案例3:结合 React useState(状态管理)

场景:React 组件中,状态是嵌套对象,需要修改深层字段(如用户年龄),同时保证状态不可变。

原生写法(需手动拷贝状态)

修改状态时需返回全新对象,嵌套层级深时,拷贝逻辑繁琐且易遗漏:

javascript 复制代码
import { useState } from "react";

function UserInfo() {
  // 嵌套状态
  const [state, setState] = useState({
    user: { name: "张三", info: { age: 20, gender: "男" } }
  });

  // 年龄+1 操作
  const increaseAge = () => {
    setState(prevState => ({
      ...prevState,
      user: {
        ...prevState.user,
        info: {
          ...prevState.user.info,
          age: prevState.user.info.age + 1
        }
      }
    }));
  };

  return (
    <div>
      <p>姓名:{state.user.name}</p>
      <p>年龄:{state.user.info.age}</p>
      <button onClick={increaseAge}>年龄+1</button>
    </div>
  );
}

Immer 写法(简化状态修改)

无需手动拷贝 prevState,直接修改 draft 即可,代码更简洁,心智负担更低:

javascript 复制代码
import { useState } from "react";
import { produce } from "immer";

function UserInfo() {
  // 嵌套状态(同原生案例)
  const [state, setState] = useState({
    user: { name: "张三", info: { age: 20, gender: "男" } }
  });

  // 年龄+1 操作(Immer 简化)
  const increaseAge = () => {
    setState(produce(draft => {
      // 直接修改深层字段,无需拷贝
      draft.user.info.age += 1;
    }));
  };

  return (
    <div>
      <p>姓名:{state.user.name}</p>
      <p>年龄:{state.user.info.age}</p>
      <button onClick={increaseAge}>年龄+1</button>
    </div>
  );
}

案例4:结合 Redux Reducer(纯函数优化)

场景:Redux 的 Reducer 是纯函数,必须返回全新状态,不能修改原状态。嵌套状态下,原生写法冗余度极高。

原生写法(多层拷贝,代码冗余)

php 复制代码
// 初始状态
const initialState = {
  user: { name: "张三", info: { age: 20, address: "北京" } },
  token: ""
};

// 原生 Reducer
function userReducer(state = initialState, action) {
  switch (action.type) {
    case "UPDATE_AGE":
      // 逐层拷贝,修改年龄
      return {
        ...state,
        user: {
          ...state.user,
          info: {
            ...state.user.info,
            age: action.payload
          }
        }
      };
    case "SET_TOKEN":
      // 修改 token(单层,相对简单)
      return { ...state, token: action.payload };
    default:
      return state;
  }
}

Immer 写法(简化 Reducer 逻辑)

用 Immer 包裹 Reducer,直接修改 draft 状态,无需手动拷贝,Reducer 逻辑更清晰:

javascript 复制代码
import { produce } from "immer";

// 初始状态(同原生案例)
const initialState = {
  user: { name: "张三", info: { age: 20, address: "北京" } },
  token: ""
};

// Immer 简化 Reducer
const userReducer = produce((draft, action) => {
  switch (action.type) {
    case "UPDATE_AGE":
      // 直接修改深层年龄字段
      draft.user.info.age = action.payload;
      break;
    case "SET_TOKEN":
      // 直接修改 token
      draft.token = action.payload;
      break;
  }
}, initialState); // 第二个参数为初始状态

三、Immer 核心优势总结

通过以上案例对比,Immer 的核心优势的可概括为三点,每一点都直击原生不可变操作的痛点:

1. 降低心智负担,代码更简洁

无需记忆"逐层拷贝"规则,不用写大量扩展运算符(...),直接用"可变写法"操作数据,嵌套层级越深,优势越明显。尤其在 Redux Reducer、React 嵌套状态修改中,能大幅减少冗余代码。

2. 减少出错概率,提升开发效率

原生操作中,嵌套层级深时容易遗漏拷贝层级,导致状态修改异常;Immer 自动处理拷贝逻辑,只需关注"要修改什么",无需关注"如何拷贝",降低出错概率,提升开发效率。

3. 性能更优,支持结构共享

Immer 并非深拷贝(JSON.parse(JSON.stringify)),而是采用"结构共享"机制------只拷贝发生变化的层级,未修改的层级直接复用原始数据,比深拷贝更高效,尤其适合大数据量场景。

四、总结

Immer 以"简单、高效、低心智负担"为核心,通过 produce API 完美解决了 JavaScript 不可变数据操作的痛点。无论是嵌套对象、数组的日常操作,还是 React、Redux 中的状态管理,Immer 都能让代码更简洁、易维护。

核心记住一句话:用 Immer,你只管"修改"草稿(draft),剩下的不可变逻辑,它来帮你搞定。

相关推荐
AI陪跑6 小时前
深入剖析:GrapesJS 中 addStyle() 导致拖放失效的问题
前端·javascript·react.js
昨晚我输给了一辆AE867 小时前
react-hook-form 初始化值为异步获取的数据的最佳实践
前端·react.js·强化学习
前端无涯7 小时前
React Router(web) 全解析:知识点、工作注意点及面试重点
前端·react.js·前端框架
AY呀7 小时前
新手必读:React组件从入门到精通,一篇文章搞定所有核心概念
前端·javascript·react.js
Ingsuifon8 小时前
ReAct智能体实现示例
前端·react.js·前端框架
IT古董8 小时前
企业级官网全栈(React·Next.js·Tailwind·Axios·Headless UI·RHF·i18n)实战教程-第四篇:登录与注册系统(核心篇)
javascript·react.js·ui
开发者小天8 小时前
React中useCallback的使用
前端·javascript·react.js·typescript·前端框架·css3·html5
开发者小天8 小时前
React中的useState传入函数的好处
前端·javascript·react.js
鹏多多9 小时前
React使用useLayoutEffect解决操作DOM页面闪烁问题
前端·javascript·react.js