相关文章推荐
打篮球的自行车  ·  /Zc:strictStrings(禁用字符 ...·  1 周前    · 
豪情万千的小刀  ·  Android ...·  昨天    · 
逆袭的移动电源  ·  杭州市萧山区教育局关于做好萧山区2022年各 ...·  10 月前    · 
慈祥的沙发  ·  书法家柳国庆:魏碑人生,浑然天成_百科TA说·  1 年前    · 
耍酷的回锅肉  ·  最高人民法院 最高人民检察院 ...·  1 年前    · 
没有腹肌的墨镜  ·  路特斯汽车宣布裁员 ...·  1 年前    · 
温柔的手套  ·  胡锡进 笑果文化 - 抖音·  1 年前    · 
Code  ›  使用Immer解决React对象深度更新的痛点开发者社区
社区功能 react const
https://cloud.tencent.com/developer/article/2324741
大气的手电筒
1 年前
Jou

使用Immer解决React对象深度更新的痛点

腾讯云
开发者社区
文档 建议反馈 控制台
首页
学习
活动
专区
工具
TVP
最新优惠活动
文章/答案/技术大牛
发布
首页
学习
活动
专区
工具
TVP 最新优惠活动
返回腾讯云官网
Jou
首页
学习
活动
专区
工具
TVP 最新优惠活动
返回腾讯云官网
社区首页 > 专栏 > 使用Immer解决React对象深度更新的痛点

使用Immer解决React对象深度更新的痛点

作者头像
Jou
发布 于 2023-09-06 13:42:14
372 0
发布 于 2023-09-06 13:42:14
举报
文章被收录于专栏: 前端技术归纳 前端技术归纳

前言

最近接到一个需求,修改一个使用 React 编写的工单系统,具体就是在创建工单的时候能配置一些增强工单通用性的功能然后把配置传给后端进行存储,乍一听其实挺简单,但是由于数据结构没设计好,写的时候非常的麻烦。

复杂对象的更新

在组件中,工单的所有参数都保存在一个对象中,像这样

const [formConfig,setFormConfig] = useState(
        type: '',// 类型
        desc: '',// 描述
        relatedPerson: { // 关联负责人
            author: {
                name: '',
                phone: ''
        fieldForm: [ // 字段
                fieldName: '',
                fieldCode: '',
                //...
                fieldName: '',
                fieldCode: '',
                //...
        //...
)

由于对象的结构很复杂,在更新的时候就尤其的麻烦。

比如,我想修改工单的表单第二个字段的名称,那我可能就需要这样写

  setFormConfig((prevState) => {
      ...prevState,
      fieldForm:prevState.fieldForm.map((item,idx) => {
          if(idx === selectIndex){
              return {
                  ...item,
                  fieldName:newName
          return item
  }); 

这样的写法不难看出很多问题:

  • 我们不得不写很多操作修改以外的代码
  • 每深入对象一层,扩展语法后的路径也需要再进一层(如 ...prevState) ,在复制粘贴过程中极易弄错弄丢
  • 由于工单的所有参数可配置,组件里面到处都充斥着这样的代码,让代码可读性变得很差。

React的心智负担

为什么要这样写?

  • React 不允许直接更改state ,而应该使用 setState
  • setState 会合并更改(merge update),所以不需要手写完整的state,但是合并仅限于 对象属性的第一级
  • setState 会 异步 地触发re-render,所以不要直接依赖 state (此时的 state.xxx 不一定是彼时的 state.xxx ),即下面的写法是有潜在bug的
  setFormConfig({
      ...formConfig,
      fieldForm:formConfig.fieldForm.map((item,idx) => {
          if(idx === 1){
              return {
                  ...item,
                  fieldName:newName
          return item
  }); 

对象深拷贝

既然不能直接在原对象上修改,那我们可以先深拷贝出一个新的对象,然后直接更改新对象的属性

  let tempFormConfig = deepClone(formConfig);
  tempFormConfig.fieldForm[1].fieldName = newName
  setFormConfig(tempFormConfig); 

这样写代码量确实减少了很多,可读性也提高不少,但是,这种方案有明显的 性能问题 —— 不管打算更新对象的哪一个属性(子节点),每次都不得不深拷贝 整个对象 ;当对象特别大的时候,深拷贝会导致性能问题。

那么怎么样避免深拷贝所有属性,而只针对目标属性(子节点)?

为了解决这种问题, Immer 来了

Immer初登场

那么 Immer 是个啥呢,用官方的话说就是

Immer (German for: always) is a tiny package that allows you to work with immutable state in a more convenient way. Immer 可以帮助我们更方便的处理不可变的状态。

怎么用呢, Immer 提供了一个 produce 方法

produce(baseState, recipe: (draftState) => void): nextState

produce 方法需要传入一个基本状态,以及一个修改传入状态的函数,在修改状态的函数中,所有标准的JavaScriptAPI都可以用于draft(草稿)对象,然后返回一个新的状态,但是原始的状态不会受到影响。

以前面修改表单配置的方法为例,使用 Immer 我们上面的状态修改就可以这样写:

import {produce} from "immer"  
setFormConfig(prevState => {
    return produce(draft => {  
        draft.fieldForm[1].fieldName = newName
})

如果你熟悉 柯里化 ,你还可以这样写

import {produce} from "immer"  
setFormConfig(produce(draft => {  
        draft.fieldForm[1].fieldName = newName
}))

是不是瞬间感觉非常的清爽,我们通过 Immer 提供的 produce 方法,可以直接像深拷贝那样,在新对象上做修改

更重要的是, 在 immer 的背后做了性能优化,而不是简单的全部深度拷贝 ,所以不用担心性能问题

Immer 的优点

Immer有着许多便捷和性能上的优势:

  • 遵循不可变数据范式,同时使用普通的JavaScript对象、数组、集合和映射,上手即用
  • 开箱即用的结构共享
  • 开箱即用对象冻结
  • 更新轻而易举
  • 冗余代码更少
  • 对JSON补丁的一流支持
  • 仅有3KB

Immer工作原理

image.png
image.png
  • 当我们调用 immer 的 API produce时,immer 将内部暂时存储着我们的目标对象(以 state 为例)
  • immer 暴露一个 draft (草稿)给我们
  • 我们在 draft 上作修改
  • immer 接收修改后的draft,immer 基于传入的 state 照着draft 的修改 返回一个新的 state

Immer Hook

如果你觉得每次调用 setState 的时候都需要配合使用一次 produce 函数很冗余,没关系, Immer 也有对应的 React Hook方法

将produce封装到useState中的 useImmer

import React, { useCallback } from "react";
import { useImmer } from "use-immer";
const TodoList = () => {
  const [todos, setTodos] = useImmer([
      id: "React",
      title: "Learn React",
      done: true
      id: "Immer",
      title: "Try Immer",
      done: false
  const handleToggle = useCallback((id) => {
    setTodos((draft) => {
      const todo = draft.find((todo) => todo.id === id);
      todo.done = !todo.done;
  }, []);
  const handleAdd = useCallback(() => {
    setTodos((draft) => {
      draft.push({
        id: "todo_" + Math.random(),
        title: "A new todo",
        done: false
  }, []);

将produce封装到useReducer中的 useImmerReducer

import React, { useCallback } from "react";
import { useImmerReducer } from "use-immer";
const TodoList = () => {
  const [todos, dispatch] = useImmerReducer(
    (draft, action) => {
      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",
            done: false
          break;
        default:
          break;
    [ /* initial todos */ ]
  );

以及配合Redux来使用

import {produce} from "immer"  
// Reducer with initial state  
const INITIAL_STATE = [  
    /* bunch of todos */  
const todosReducer = produce((draft, action) => {  
    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",  
            done: false  
 
推荐文章
打篮球的自行车  ·  /Zc:strictStrings(禁用字符串文本类型转换) | Microsoft Learn
1 周前
豪情万千的小刀  ·  Android ConstraintLayout layout_constrainedWidth使用
昨天
逆袭的移动电源  ·  杭州市萧山区教育局关于做好萧山区2022年各类高中招生工作的通知
10 月前
慈祥的沙发  ·  书法家柳国庆:魏碑人生,浑然天成_百科TA说
1 年前
耍酷的回锅肉  ·  最高人民法院 最高人民检察院 司法部印发《关于进一步规范法院、检察院离任人员从事律师职业的意见》的通知  广东省司法厅网站
1 年前
没有腹肌的墨镜  ·  路特斯汽车宣布裁员 国内销量堪忧_车家号_发现车生活_汽车之家
1 年前
温柔的手套  ·  胡锡进 笑果文化 - 抖音
1 年前
今天看啥   ·   Py中国   ·   codingpro   ·   小百科   ·   link之家   ·   卧龙AI搜索
删除内容请联系邮箱 2879853325@qq.com
Code - 代码工具平台
© 2024 ~ 沪ICP备11025650号