React 状态更新:如何避免为嵌套数据写一长串 ...?

在 React 开发中,组件的状态管理是一个基础且重要的部分。开发者需要理解状态如何工作,以及在不同情况下如何正确地更新状态。当状态的数据结构较为复杂,比如包含多层嵌套的对象或数组时,更新操作会变得繁琐。本文将介绍这个问题的根源,并探讨几种解决方案,包括 use-immer 和使用状态管理库如 Zustand 或 Redux。

状态不可变性的概念

React 的设计依赖于一个原则,即状态的不可变性。这意味着当需要更新一个对象或数组类型的状态时,不能直接修改原有的数据。必须创建一个新的对象或数组来代表更新后的状态。这样做是为了让 React 能够检测到状态的变化,从而触发组件的重新渲染。

例如,有一个用户信息对象:

javascript 复制代码
const user = { name: "Alice", age: 25 };

如果要将年龄更新为 26,不能直接写 user.age = 26。这会修改原始对象,React 无法感知这种变化。正确的方式是创建一个新对象:

javascript 复制代码
const newUser = { ...user, age: 26 };

这个新对象包含了原始对象的所有属性,并且 age 属性被更新。原始的 user 对象保持不变。

使用 useState 处理嵌套状态

useState 是 React 提供的一个 Hook,用于在函数组件中添加状态。当状态是一个包含多层嵌套的结构时,更新其中的某个深层属性需要逐层复制对象。

假设有一个更复杂的用户状态:

javascript 复制代码
const [user, setUser] = useState({
  name: "Alice",
  profile: {
    age: 25,
    address: {
      city: "Beijing",
      street: "Main St"
    }
  },
  preferences: {
    theme: "dark"
  }
});

如果要更新地址中的street,代码会是这样:

javascript 复制代码
const updateStreet = (newStreet) => {
  setUser(prevUser => ({
    ...prevUser,
    profile: {
      ...prevUser.profile,
      address: {
        ...prevUser.profile.address,
        street: newStreet
      }
    }
  }));
};

这段代码从最外层开始,每一层都使用展开语法复制原有的属性,直到需要修改的层级。这种写法在嵌套层次较深时,代码会变得冗长,容易出错。开发者需要确保每一层都正确地展开了原有属性,否则可能会导致引用被共享,从而引发难以发现的问题。

use-immer 的作用

use-immer 是一个第三方库,它提供了一个 Hook,允许开发者使用一种看似可变的方式来更新状态,而内部会自动处理不可变性的要求。它基于 immer 库实现,利用了 JavaScript 的 Proxy 特性。

使用 use-immer,上面的状态可以这样定义:

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

const [user, setUser] = useImmer({
  name: "Alice",
  profile: {
    age: 25,
    address: {
      city: "Beijing",
      street: "Main St"
    }
  },
  preferences: {
    theme: "dark"
  }
});

此时,修改 user.profile.address.street 的代码可以简化为:

javascript 复制代码
const updateStreet = (newStreet) => {
  setUser(draft => {
    draft.profile.address.street = newStreet;
  });
};

setUser 的回调函数中,参数 draft 是一个代理对象。对 draft 的修改会被 immer 捕获,并生成一个新的不可变状态。开发者可以像修改普通对象一样写代码,但最终的结果是符合不可变性原则的。

use-immer 的工作原理

use-immer 的核心是 immer 库。当 setUser 被调用时,传入的回调函数接收一个 draft 对象。这个 draft 对象是原始状态的一个代理。在回调函数执行期间,对 draft 的任何修改都会被记录。immer 会根据这些修改,创建一个新的状态对象。在创建新对象的过程中,未被修改的部分会复用原始对象的引用,这被称为结构共享,有助于提高性能。

使用状态管理库

对于应用中需要在多个组件间共享的状态,可以考虑使用状态管理库,如 Zustand 或 Redux。这些库可以将状态集中管理,避免通过组件层级逐层传递。

Zustand

Zustand 是一个轻量级的状态管理库。它可以与 immer 结合使用,提供简洁的 API。首先,创建一个 store:

javascript 复制代码
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';

const useUserStore = create(
  immer((set) => ({
    user: {
      name: "Alice",
      // ... 其他属性
    },
    updateUser: (updater) =>
      set((state) => {
        updater(state.user);
      })
  }))
);

在这个 store 中,user 是状态,updateUser 是一个函数,它接收一个更新函数作为参数。在组件中,可以从 store 中读取状态和更新函数:

javascript 复制代码
function UserProfile() {
  const user = useUserStore(state => state.user);
  const updateUser = useUserStore(state => state.updateUser);

  const changeCity = () => {
    updateUser(draft => {
      draft.profile.address.city = "Shanghai";
    });
  };

  return (
    <div>
      <p>{user.profile.address.city}</p>
      <button onClick={changeCity}>Change City</button>
    </div>
  );
}

useUserStore 返回的状态和函数可以在任何组件中使用。当状态更新时,使用该状态的组件会重新渲染。

Redux

Redux 是一个更传统的状态管理方案。使用 Redux Toolkit 可以简化 Redux 的使用。定义一个 slice:

javascript 复制代码
import { createSlice } from '@reduxjs/toolkit';

const userSlice = createSlice({
  name: 'user',
  initialState: {
    user: {
      name: "Alice",
      // ... 其他属性
    }
  },
  reducers: {
    updateUser: (state, action) => {
      // action.payload 包含更新函数
      action.payload(state.user);
    }
  }
});

export const { updateUser } = userSlice.actions;
export default userSlice.reducer;

在组件中,使用 useSelector 读取状态,useDispatch 发送 action:

javascript 复制代码
import { useSelector, useDispatch } from 'react-redux';
import { updateUser } from './userSlice';

function UserProfile() {
  const user = useSelector(state => state.user.user);
  const dispatch = useDispatch();

  const changeCity = () => {
    dispatch(updateUser(draft => {
      draft.profile.address.city = "Shanghai";
    }));
  };

  return (
    <div>
      <p>{user.profile.address.city}</p>
      <button onClick={changeCity}>Change City</button>
    </div>
  );
}

总结

当 React 组件的状态结构复杂时,使用 useState 直接更新嵌套属性会导致代码冗长且易错。use-immer 提供了一种简化的方式,允许开发者使用直接赋值的语法,而内部保证状态的不可变性。对于需要在多个组件间共享的状态,可以使用 Zustand 或 Redux 等状态管理库。这些库可以将状态集中存放,并提供机制来更新和读取状态。Zustand 的 API 较为简洁,Redux 功能更全面,适用于大型应用。选择哪种方案取决于应用的具体需求和复杂度。

相关推荐
三*一1 分钟前
基于 Turf.js 实现高精度多边形修整工具(模拟 ArcGIS 修整功能)
开发语言·前端·javascript·arcgis·maobox gl·turf.js
踩着两条虫2 分钟前
VTJ.PRO 在线应用开发平台的工作台与后台管理视图
前端·人工智能·ai编程
踩着两条虫2 分钟前
VTJ.PRO 在线应用开发平台多平台运行时(Web, H5, UniApp)
前端·低代码·ai编程
ZC19959210 分钟前
Node.js npm 安装过程中 EBUSY 错误的分析与解决方案
前端·npm·node.js
升鲜宝供应链及收银系统源代码服务11 分钟前
生鲜配送供应链管理系统源代码之升鲜宝社区团购商城小程序(一)
java·前端·数据库·小程序·notepad++·供应链系统源代码·多门店收银系统
ghhgy53115 分钟前
Chrome、Edge浏览器显示“由贵组织管理”,删除、解决方法
前端·chrome·edge
533_17 分钟前
[svg] fill-opacity、stroke-opacity与opacity
前端
九天轩辕17 分钟前
Chromium 内核深度剖析:HTML 属性解析限制导致的视频静音失效问题
前端·html·音视频
cmdyu_18 分钟前
Chrome 132+ 篡改猴脚本不生效的排查与解决
前端·chrome
曹牧19 分钟前
Java:解析Json字符串格式要求
java·linux·运维·前端