React 更新state中的对象

news2025/4/19 9:02:42

更新 state 中的对象

state 中可以保存任意类型的 JavaScript 值,包括对象。但是,你不应该直接修改存放在 React state 中的对象。相反,当你想要更新一个对象时,你需要创建一个新的对象(或者将其拷贝一份),然后将 state 更新为此对象。

文章及例子是参考React官方文档教程,开发环境:React+ts+antd

学习内容:

  • 如何正确地更新 React state 中的对象
  • 如何在不产生 mutation 的情况下更新一个嵌套对象
  • 什么是不可变性(immutability),以及如何不破坏它
  • 如何使用 Immer 使复制对象不那么繁琐

什么是 mutation?

你可以在 state 中存放任意类型的 JavaScript 值。

const [x, setX] = useState(0);
setX(5);

state x 从 0 变为 5,但是数字 0 本身并没有发生改变。在 JavaScript 中,无法对内置的原始值,如数字、字符串和布尔值,进行任何更改。

现在考虑 state 中存放对象的情况:

const [position, setPosition] = useState({ x: 0, y: 0 });

从技术上来讲,可以改变对象自身的内容。当你这样做时,就制造了一个 mutation:

position.x = 5;

然而,虽然严格来说 React state 中存放的对象是可变的,但你应该像处理数字、布尔值、字符串一样将它们视为不可变的。因此你应该替换它们的值,而不是对它们进行修改。

将 state 视为只读的

换句话说,你应该 把所有存放在 state 中的 JavaScript 对象都视为只读的。

在下面的例子中,我们用一个存放在 state 中的对象来表示指针当前的位置。当你在预览区触摸或移动光标时,红点会跟随着你的指针移动:

import React from 'react';
import {useState} from "react";

const App: React.FC = () => {
    const [position, setPosition] = useState({
        x: 0,
        y: 0
    });
    return (
        <div
            onPointerMove={e => {
                setPosition({
                    x: e.clientX,
                    y: e.clientY
                });
            }}
            style={{
                position: 'relative',
                width: '100vw',
                height: '100vh',
            }}>
            <div style={{
                position: 'absolute',
                backgroundColor: 'red',
                borderRadius: '50%',
                transform: `translate(${position.x}px, ${position.y}px)`,
                left: -10,
                top: -10,
                width: 20,
                height: 20,
            }}/>
        </div>
    );
};

export default App;

使用展开语法复制对象

在之前的例子中,始终会根据当前指针的位置创建出一个新的 position 对象。但是通常,你会希望把 现有 数据作为你所创建的新对象的一部分。例如,你可能只想要更新表单中的一个字段,其他的字段仍然使用之前的值。

下面的代码中,输入框并不会正常运行,因为 onChange 直接修改了 state :

import React from 'react';
import {useState} from "react";

const App: React.FC = () => {
    const [person, setPerson] = useState({
        firstName: 'Barbara',
        lastName: 'Hepworth',
        email: 'bhepworth@sculpture.com'
    });
    const handleFirstNameChange=(e:any)=> {
        person.firstName = e.target.value;
    }

    const handleLastNameChange=(e:any)=> {
        person.lastName = e.target.value;
    }

    const handleEmailChange=(e:any)=> {
        person.email = e.target.value;
    }
    return (
        <>
            <label>
                First name:
                <input
                    value={person.firstName}
                    onChange={handleFirstNameChange}
                />
            </label>
            <label>
                Last name:
                <input
                    value={person.lastName}
                    onChange={handleLastNameChange}
                />
            </label>
            <label>
                Email:
                <input
                    value={person.email}
                    onChange={handleEmailChange}
                />
            </label>
            <p>
                {person.firstName}{' '}
                {person.lastName}{' '}
                ({person.email})
            </p>
        </>
    );
};

export default App;

在这里插入图片描述
此时输入框是不会正常运行的.
例如,下面这行代码修改了上一次渲染中的 state:

person.firstName = e.target.value;

想要实现你的需求,最可靠的办法就是创建一个新的对象并将它传递给 setPerson。但是在这里,你还需要 把当前的数据复制到新对象中,因为你只改变了其中一个字段:

setPerson({
  firstName: e.target.value, // 从 input 中获取新的 first name
  lastName: person.lastName,
  email: person.email
});

你可以使用 … 对象展开 语法,这样你就不需要单独复制每个属性。

setPerson({
  ...person, // 复制上一个 person 中的所有字段
  firstName: e.target.value // 但是覆盖 firstName 字段 
});

在这里插入图片描述
改变输入后

在这里插入图片描述
请注意 … 展开语法本质是是“浅拷贝”——它只会复制一层。这使得它的执行速度很快,但是也意味着当你想要更新一个嵌套属性时,你必须得多次使用展开语法。

更新一个嵌套对象

考虑下面这种结构的嵌套对象:

const [person, setPerson] = useState({
  name: 'Niki de Saint Phalle',
  artwork: {
    title: 'Blue Nana',
    city: 'Hamburg',
    image: 'https://i.imgur.com/Sd1AgUOm.jpg',
  }
});

如果你想要更新 person.artwork.city 的值,用 mutation 来实现的方法非常容易理解:

person.artwork.city = 'New Delhi';

但是在 React 中,你需要将 state 视为不可变的!为了修改 city 的值,你首先需要创建一个新的 artwork 对象(其中预先填充了上一个 artwork 对象中的数据),然后创建一个新的 person 对象,并使得其中的 artwork 属性指向新创建的 artwork 对象:

const nextArtwork = { ...person.artwork, city: 'New Delhi' };
const nextPerson = { ...person, artwork: nextArtwork };
setPerson(nextPerson);

或者,写成一个函数调用:

setPerson({
  ...person, // 复制其它字段的数据 
  artwork: { // 替换 artwork 字段 
    ...person.artwork, // 复制之前 person.artwork 中的数据
    city: 'New Delhi' // 但是将 city 的值替换为 New Delhi!
  }
});

这虽然看起来有点冗长,但对于很多情况都能有效地解决问题

使用 Immer 编写简洁的更新逻辑

如果你的 state 有多层的嵌套,你或许应该考虑 将其扁平化。但是,如果你不想改变 state 的数据结构,你可能更喜欢用一种更便捷的方式来实现嵌套展开的效果。Immer 是一个非常流行的库,它可以让你使用简便但可以直接修改的语法编写代码,并会帮你处理好复制的过程。通过使用 Immer,你写出的代码看起来就像是你“打破了规则”而直接修改了对象:

updatePerson(draft => {
  draft.artwork.city = 'Lagos';
});

但是不同于一般的 mutation,它并不会覆盖之前的 state!

Immer 是如何运行的?

由 Immer 提供的 draft 是一种特殊类型的对象,被称为 Proxy,它会记录你用它所进行的操作。这就是你能够随心所欲地直接修改对象的原因所在!从原理上说,Immer 会弄清楚 draft 对象的哪些部分被改变了,并会依照你的修改创建出一个全新的对象。

尝试使用 Immer:

  1. 运行 npm install use-immer 添加 Immer 依赖
  2. 用 import { useImmer } from ‘use-immer’ 替换掉 import { useState } from ‘react’

下面我们把上面的例子用 Immer 实现一下:

import React from 'react';
import { useImmer } from 'use-immer';

const App: React.FC = () => {
    const [person, updatePerson] = useImmer({
        name: 'Niki de Saint Phalle',
        artwork: {
            title: 'Blue Nana',
            city: 'Hamburg',
        }
    });
    const handleNameChange=(e:any)=> {
        updatePerson(draft => {
            draft.name = e.target.value;
        });
    }

    const handleTitleChange=(e:any)=> {
        updatePerson(draft => {
            draft.artwork.title = e.target.value;
        });
    }
    const handleCityChange=(e:any)=>{
        updatePerson(draft => {
            draft.artwork.city = e.target.value;
        });
    }

    return (
        <>
            <label>
                Name:
                <input
                    value={person.name}
                    onChange={handleNameChange}
                />
            </label>
            <br/>
            <label>
                Title:
                <input
                    value={person.artwork.title}
                    onChange={handleTitleChange}
                />
            </label>
            <br/>
            <label>
                City:
                <input
                    value={person.artwork.city}
                    onChange={handleCityChange}
                />
            </label>
            <p>
                <i>{person.artwork.title}</i>
                {' by '}
                {person.name}
                <br/>
                (located in {person.artwork.city})
            </p>
        </>
    );
};

export default App;

在这里插入图片描述
改变输入内容:
在这里插入图片描述

可以看到,事件处理函数变得更简洁了。你可以随意在一个组件中同时使用 useState 和 useImmer。如果你想要写出更简洁的更新处理函数,Immer 会是一个不错的选择,尤其是当你的 state 中有嵌套,并且复制对象会带来重复的代码时。

为什么在 React 中不推荐直接修改 state?

有以下几个原因:

  • 调试:如果你使用 console.log 并且不直接修改 state,你之前日志中的 state 的值就不会被新的 state 变化所影响。这样你就可以清楚地看到两次渲染之间 state 的值发生了什么变化
  • 优化:React 常见的 优化策略 依赖于如果之前的 props 或者 state 的值和下一次相同就跳过渲染。如果你从未直接修改 state ,那么你就可以很快看到 state 是否发生了变化。如果 prevObj === obj,那么你就可以肯定这个对象内部并没有发生改变。
  • 新功能:我们正在构建的 React 的新功能依赖于 state 被 像快照一样看待 的理念。如果你直接修改 state 的历史版本,可能会影响你使用这些新功能。
  • 需求变更:有些应用功能在不出现任何修改的情况下会更容易实现,比如实现撤销/恢复、展示修改历史,或是允许用户把表单重置成某个之前的值。这是因为你可以把 state 之前的拷贝保存到内存中,并适时对其进行再次使用。如果一开始就用了直接修改 state 的方式,那么后面要实现这样的功能就会变得非常困难。
  • 更简单的实现:React 并不依赖于 mutation ,所以你不需要对对象进行任何特殊操作。它不需要像很多“响应式”的解决方案一样去劫持对象的属性、总是用代理把对象包裹起来,或者在初始化时做其他工作。这也是 React 允许你把任何对象存放在 state 中——不管对象有多大——而不会造成有任何额外的性能或正确性问题的原因。

摘要

  • 将 React 中所有的 state 都视为不可直接修改的。
  • 当你在 state 中存放对象时,直接修改对象并不会触发重渲染,并会改变前一次渲染“快照”中 state 的值。
  • 不要直接修改一个对象,而要为它创建一个 新 版本,并通过把 state 设置成这个新版本来触发重新渲染。
  • 你可以使用这样的 {…obj, something: ‘newValue’} 对象展开语法来创建对象的拷贝。
  • 对象的展开语法是浅层的:它的复制深度只有一层。
  • 想要更新嵌套对象,你需要从你更新的位置开始自底向上为每一层都创建新的拷贝。
  • 想要减少重复的拷贝代码,可以使用 Immer。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2336315.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

算法思想之模拟

欢迎拜访&#xff1a;雾里看山-CSDN博客 本篇主题&#xff1a;算法思想之模拟 发布时间&#xff1a;2025.4.14 隶属专栏&#xff1a;算法 目录 算法介绍核心特点常见问题优化方向 例题替换所有的问号题目链接题目描述算法思路代码实现 提莫攻击题目链接题目描述算法思路代码实现…

测试基础笔记第四天(html)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 html介绍1. 介绍2.骨架标签3.常用标签标题标签段落标签超链接标签图片标签换行和空格标签布局标签input标签&#xff08;变形金刚&#xff09;form标签列表标签 htm…

WPF 中的元素继承层次结构 ,以下是对图中内容的详细说明:

顶层基类 DispatcherObject&#xff1a;处于继承体系最顶端&#xff0c;是一个抽象类。它为 WPF 元素提供了与 Dispatcher&#xff08;调度器&#xff09;交互的能力&#xff0c;Dispatcher 负责管理线程间的消息传递&#xff0c;确保 UI 操作在正确的线程&#xff08;通常是 …

十九、UDP编程和IO多路复用

1、UDP编程 服务端&#xff1a; #include<stdio.h> #include <arpa/inet.h> #include<stdlib.h> #include<string.h> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <pthread.h> #include &l…

DeepSeek使用001:Word中配置DeepSeek AI的V3和R1模型

文章目录 Word中配置DeepSeek大模型1、勾选开发工具2、信任中心设置3、添加DeepSeek-V3模型4、获取API KEY5、添加DeepSeek-R1模型6、新建组7、测试使用 Word中配置DeepSeek大模型 1、勾选开发工具 打开【选项】 选择【自定义功能区】 2、信任中心设置 打开【信任中心】&…

华为OD机试真题——攀登者2(2025A卷:200分)Java/python/JavaScript/C++/C语言/GO六种最佳实现

2025 A卷 200分 题型 本文涵盖详细的问题分析、解题思路、代码实现、代码详解、测试用例以及综合分析&#xff1b; 并提供Java、python、JavaScript、C、C语言、GO六种语言的最佳实现方式&#xff01; 华为OD机试真题《攀登者2》&#xff1a; 目录 题目名称&#xff1a;攀登者2…

Windows卸载重装Docker

卸载 删除C:\Program Files\Docker &#xff0c;如果更改了路径的就找到相关位置进行删除 删除 C:\Users\<用户名>\.docker 清理注册表&#xff0c;不然重装会报错 Exising installation is up to date 按下WindowR唤起命令输入界面&#xff0c;输入regedit打开注…

双目视觉中矩阵等参数说明及矫正

以下是标定文件中各个参数的详细解释&#xff1a; 1. 图像尺寸 (imageSize) 参数值: [1280, 1024]含义: 相机的图像分辨率&#xff0c;宽度为1280像素&#xff0c;高度为1024像素。 2. 相机内参矩阵 (leftCameraMatrix / rightCameraMatrix) 结构: yaml data: [fx, 0, cx, 0,…

PyTorch核心函数详解:gather与where的实战指南

PyTorch中的torch.gather和torch.where是处理张量数据的关键工具&#xff0c;前者实现基于索引的灵活数据提取&#xff0c;后者完成条件筛选与动态生成。本文通过典型应用场景和代码演示&#xff0c;深入解析两者的工作原理及使用技巧&#xff0c;帮助开发者提升数据处理的灵活…

Go:接口

接口既约定 Go 语言中接口是抽象类型 &#xff0c;与具体类型不同 &#xff0c;不暴露数据布局、内部结构及基本操作 &#xff0c;仅提供一些方法 &#xff0c;拿到接口类型的值 &#xff0c;只能知道它能做什么 &#xff0c;即提供了哪些方法 。 func Fprintf(w io.Writer, …

ESP32+Arduino入门(三):连接WIFI获取当前时间

ESP32内置了WIFI模块连接WIFI非常简单方便。 代码如下&#xff1a; #include <WiFi.h>const char* ssid "WIFI名称"; const char* password "WIFI密码";void setup() {Serial.begin(115200);WiFi.begin(ssid,password);while(WiFi.status() ! WL…

CSS高度坍塌?如何解决?

一、什么是高度坍塌&#xff1f; 高度坍塌&#xff08;Collapsing Margins&#xff09;是指当父元素没有设置边框&#xff08;border&#xff09;、内边距&#xff08;padding&#xff09;、内容&#xff08;content&#xff09;或清除浮动时&#xff0c;其子元素的 margin 会…

【数据结构】之散列

一、定义与基本术语 &#xff08;一&#xff09;、定义 散列&#xff08;Hash&#xff09;是一种将键&#xff08;key&#xff09;通过散列函数映射到一个固定大小的数组中的技术&#xff0c;因为键值对的映射关系&#xff0c;散列表可以实现快速的插入、删除和查找操作。在这…

空地机器人在复杂动态环境下,如何高效自主导航?

随着空陆两栖机器人(AGR)在应急救援和城市巡检等领域的应用范围不断扩大&#xff0c;其在复杂动态环境中实现自主导航的挑战也日益凸显。对此香港大学王俊铭基于阿木实验室P600无人机平台自主搭建了一整套空地两栖机器人&#xff0c;使用Prometheus开源框架完成算法的仿真验证与…

第二十一讲 XGBoost 回归建模 + SHAP 可解释性分析(利用R语言内置数据集)

下面我将使用 R 语言内置的 mtcars 数据集&#xff0c;模拟一个完整的 XGBoost 回归建模 SHAP 可解释性分析 实战流程。我们将以预测汽车的油耗&#xff08;mpg&#xff09;为目标变量&#xff0c;构建 XGBoost 模型&#xff0c;并用 SHAP 来解释模型输出。 &#x1f697; 示例…

数据分析实战案例:使用 Pandas 和 Matplotlib 进行居民用水

原创 IT小本本 IT小本本 2025年04月15日 18:31 北京 本文将使用 Matplotlib 及 Seaborn 进行数据可视化。探索如何清理数据、计算月度用水量并生成有价值的统计图表&#xff0c;以便更好地理解居民的用水情况。 数据处理与清理 读取 Excel 文件 首先&#xff0c;我们使用 pan…

hash.

Redis 自身就是键值对结构 Redis 自身的键值对结构就是通过 哈希 的方式来组织的 哈希类型中的映射关系通常称为 field-value&#xff0c;用于区分 Redis 整体的键值对&#xff08;key-value&#xff09;&#xff0c; 注意这里的 value 是指 field 对应的值&#xff0c;不是键…

记录鸿蒙应用上架应用未配置图标的前景图和后景图标准要求尺寸1024px*1024px和标准要求尺寸1024px*1024px

审核报错【①应用未配置图标的前景图和后景图,标准要求尺寸1024px*1024px且需下载HUAWEI DevEco Studio 5.0.5.315或以上版本进行图标再处理、②应用在展开状态下存在页面左边距过大的问题, 应用在展开状态下存在页面右边距过大的问题, 当前页面左边距: 504 px, 当前页面右边距…

Google最新《Prompt Engineering》白皮书全解析

近期有幸拿到了Google最新发布的《Prompt Engineering》白皮书&#xff0c;这是一份由Lee Boonstra主笔&#xff0c;Michael Sherman、Yuan Cao、Erick Armbrust、Antonio Gulli等多位专家共同贡献的权威性指南&#xff0c;发布于2025年2月。今天我想和大家分享这份68页的宝贵资…

如何快速部署基于Docker 的 OBDIAG 开发环境

很多开发者对 OceanBase的 SIG社区小组很有兴趣&#xff0c;但如何将OceanBase的各类工具部署在开发环境&#xff0c;对于不少开发者而言都是比较蛮烦的事情。例如&#xff0c;像OBDIAG&#xff0c;其在WINDOWS系统上配置较繁琐&#xff0c;需要单独搭建C开发环境。此外&#x…