React & Immer 不可变数据结构的处理

Immer 是一个 immutable 库,核心实现是利用 ES6 的 proxy,几乎以最小的成本实现了 js 的不可变数据结构,简单易用、体量小巧、设计巧妙,满足了我们对JS不可变数据结构的需求。

无奈网络上完善的文档实在太少,所以自己写了一份,本篇文章以贴近实战的思路和流程,对 Immer 进行了全面的讲解。

Immer 可以在需要使用不可变数据结构的任何上下文中使用。例如与 React state、React 或 Redux reducers 或者 configuration management 结合使用。不可变的数据结构允许(高效)的变化检测:如果对对象的引用没有改变,那么对象本身也没有改变。此外,它使克隆对象相对便宜:数据树的未更改部分不需要复制,并且在内存中与相同状态的旧版本共享

一般来说,这些好处可以通过确保您永远不会更改对象、数组或映射的任何属性来实现,而是始终创建一个更改后的副本。在实践中,这可能会导致代码编写起来非常麻烦,并且很容易意外违反这些约束。 Immer 将通过解决以下痛点来帮助您遵循不可变数据范式:

  1. Immer 将检测到意外 mutations 并抛出错误。
  2. Immer 将不再需要创建对不可变对象进行深度更新时所需的典型样板代码:如果没有 Immer,则需要在每个级别手动制作对象副本。通常通过使用大量 ... 展开操作。使用 Immer 时,会对 draft 对象进行更改,该对象会记录更改并负责创建必要的副本,而不会影响原始对象。
  3. 使用 Immer 时,您无需学习专用 API 或数据结构即可从范例中受益。使用 Immer,您将使用纯 JavaScript 数据结构,并使用众所周知的安全地可变 JavaScript API。

useState + Immer

useState hook 假定存储在其中的任何 state 都被视为不可变的。使用 Immer 可以大大简化 React 组件状态的深度更新。

复制代码
import { useCallback, useState } from 'react';
import { produce } from 'immer';

const TodoList = () => {
  const [todos, setTodos] = useState([
    {
      id: 'React',
      title: 'Learn React',
      done: true
    },
    {
      id: 'Immer',
      title: 'Try Immer',
      done: false
    }
  ]);

  const handleToggle = useCallback((id: any) => {
    setTodos(
      produce((draft) => {
        const todo: any = draft.find((todo) => todo.id === id);
        todo.done = !todo?.done
      })
    );
  }, []);

  const handleAdd = useCallback(() => {
    setTodos(
      produce((draft) => {
        draft.push({
          id: 'todo_' + Math.random(),
          title: 'a new todo',
          done: false
        })
      })
    );
  }, [])

  return (
    <div>
      <button onClick={handleAdd}>新增</button>
      <div className="list-c">
        {todos.map(item => {
          return (
            <ul key={item.id} className={item.done ? 'done' : ''}>
              <li>{item.id}</li>
              <li>{item.title}</li>
              <li style={{color: 'blue', paddingLeft: '20px', cursor: 'pointer'}} onClick={()=>handleToggle(item.id)}>change done</li>
            </ul>
          )
        })}
      </div>
    </div>
  )
}

export default TodoList;

useImmer

可以省掉 produce 包装的更新模式。

复制代码
import { useCallback } from 'react';
import { useImmer } from 'use-immer';

const TodoList = () => {
  const [todos, setTodos] = useImmer([
    {
      id: 'React2',
      title: 'Learn React 2',
      done: true
    },
    {
      id: 'Immer2',
      title: 'Try Immer 2',
      done: false
    }
  ]);

  const handleToggle = useCallback((id: any) => {
    setTodos((draft) => {
      const todo: any = draft.find((todo) => todo.id === id);
      todo.done = !todo.done;
    })
  }, [])

  const handleAdd = useCallback(() => {
    setTodos((draft) => {
      draft.push({
        id: 'todo2_' + Math.random(),
        title: 'A new todo 2',
        done: false
      })
    })
  }, [])
  
  return (
    <div>
      <button onClick={handleAdd}>新增</button>
      <div className="list-c">
        {todos.map(item => {
          return (
            <ul key={item.id} className={item.done ? 'done' : ''}>
              <li>{item.id}</li>
              <li>{item.title}</li>
              <li style={{color: 'blue', paddingLeft: '20px', cursor: 'pointer'}} onClick={()=>handleToggle(item.id)}>change done</li>
            </ul>
          )
        })}
      </div>
    </div>
  )
}

export default TodoList;

useReducer + Immer

useState 类似,需要使用 produce 包装的更新模式,useReducer 也与 Immer 巧妙结合

复制代码
import { useCallback, useReducer } from 'react';
import { produce } from 'immer';

const TodoList = () => {
  const [todos, dispatch] = useReducer(
    produce((draft, action) => {
      switch (action.type) {
        case 'toggle':
          const todo: any = draft.find(todo => todo.id === action.id);
          todo.done = !todo.done;
          break;
        case 'add':
          draft.push({
            id: action.id,
            title: 'A new todo 3',
            done: false
          })
          break;
        default:
          break;
      }
    }),
    [
      {
        id: 'React3',
        title: 'Learn React 3',
        done: true
      },
      {
        id: 'Immer3',
        title: 'Try Immer3',
        done: false
      }
    ]
  )

  const handleToggle = useCallback((id) => {
    dispatch({
      type: 'toggle',
      id: id
    })
  }, [])

  const handleAdd = useCallback(() => {
    dispatch({
      type: 'add',
      id: 'todo3_' + Math.random()
    })
  }, [])
  
  return (
    <div>
      <button onClick={handleAdd}>新增</button>
      <div className="list-c">
        {todos.map(item => {
          return (
            <ul key={item.id} className={item.done ? 'done' : ''}>
              <li>{item.id}</li>
              <li>{item.title}</li>
              <li style={{color: 'blue', paddingLeft: '20px', cursor: 'pointer'}} onClick={()=>handleToggle(item.id)}>change done</li>
            </ul>
          )
        })}
      </div>
    </div>
  )
}

export default TodoList;

useImmerReducer

useImmerReducer Reducer方式省去 produce 包装的更新模式。

复制代码
import { useCallback } from 'react';
import { useImmerReducer } from 'use-immer';

const TodoList = () => {
  const [todos, dispatch] = useImmerReducer(
    (draft, action: any) => {
      switch (action.type) {
        case 'toggle':
          const todo = draft.find((todo) => todo.id === action.id);
          todo.done = !todo.done;
          break;
        case 'add':
          draft.push({
            id: action.id,
            title: 'A new todo 4',
            done: false
          })
          break;
        case default:
          break;
      }
    },
    [
      {
        id: 'React4',
        title: 'Learn React 4',
        done: true
      },
      {
        id: 'Immer4',
        title: 'Try Immer4',
        done: false
      }
    ]
  );

  const handleToggle = useCallback((id) => {
    dispatch({
      type: 'toggle',
      id
    })
  }, []);

  const handleAdd = useCallback(() => {
    dispatch({
      type: 'add',
      id: 'todo4_' + Math.random()
    })
  }, []);
  
  return (
    <div>
      <button onClick={handleAdd}>新增</button>
      <div className="list-c">
        {todos.map(item => {
          return (
            <ul key={item.id} className={item.done ? 'done' : ''}>
              <li>{item.id}</li>
              <li>{item.title}</li>
              <li style={{color: 'blue', paddingLeft: '20px', cursor: 'pointer'}} onClick={()=>handleToggle(item.id)}>change done</li>
            </ul>
          )
        })}
      </div>
    </div>
  )
}

export default TodoList;

vue2 项目升级 vue3 之 gogocode 代码转换规则覆盖&具体实践情况(二)-CSDN博客

相关推荐
雪隐13 分钟前
个人电脑玩AI-16让5060 Ti给你打工——MiniMax H3 提速实录:从"泡杯茶等视频"到"视频等你",一张 5060 Ti 的自我修养
前端·人工智能·后端
FungLeo33 分钟前
Flutter Web 中文字体困境与渲染器选择:CanvasKit vs HTML renderer 实战
前端·flutter·html
敲代码的玉米C1 小时前
我修的那个 bug,制造了另一个 bug
前端·人工智能·架构
wabs6661 小时前
关于图论【最短路径之Bellman_ford 算法(判断负权回路)|卡码网95.城市间货物运输II的思考】
数据结构·算法·图论·bellman_ford算法·卡码网·判断负权回路
gis开发之家1 小时前
《Vue3 从入门到大神50篇》Vue3 源码详解(二十):生命周期钩子源码解析 —— onMounted / onUpdated 如何实现?
前端·javascript·前端框架·vue3·vue3源码
Narrastory1 小时前
我用 Claude Code 写代码不到 2 小时,却花了 3 天做完这个软件—Vibe Coding 的正确姿势,是设计不是生成
前端·人工智能·github
Enaium1 小时前
我实现了KMP的SDL绑定并编写了高性能软光追
前端·kotlin
古夕1 小时前
本地正常线上白屏:一次路由切换后刷新恢复问题的排查与修复
前端·ai编程
栀鸢ouo1 小时前
解决Element Plus表格展开行横向溢出、滚动截断问题(项目实战方案)
前端·vue.js
用户39051332192881 小时前
写了5年JS,才发现这10个方法能少写一半代码
前端