React 更新state中的對象

更新 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 (<divonPointerMove={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:<inputvalue={person.firstName}onChange={handleFirstNameChange}/></label><label>Last name:<inputvalue={person.lastName}onChange={handleLastNameChange}/></label><label>Email:<inputvalue={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 namelastName: 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:<inputvalue={person.name}onChange={handleNameChange}/></label><br/><label>Title:<inputvalue={person.artwork.title}onChange={handleTitleChange}/></label><br/><label>City:<inputvalue={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.pswp.cn/news/901486.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/901486.shtml
英文地址,請注明出處:http://en.pswp.cn/news/901486.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

基于 GoFrame 框架的電子郵件發送實踐:優勢、特色與經驗分享

1. 引言 如果你是一位有1-2年Go開發經驗的后端開發者&#xff0c;可能已經熟悉了Go語言在性能和并發上的天然優勢&#xff0c;也曾在項目中遇到過郵件發送的需求——無論是用戶注冊時的激活郵件、系統異常時的通知&#xff0c;還是營銷活動中的批量促銷郵件&#xff0c;郵件功…

AndroidStudio編譯報錯 Duplicate class kotlin

具體的編譯報錯信息如下&#xff1a; Duplicate class kotlin.collections.jdk8.CollectionsJDK8Kt found in modules kotlin-stdlib-1.8.10 (org.jetbrains.kotlin:kotlin-stdlib:1.8.10) and kotlin-stdlib-jdk8-1.6.21 (org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.21) D…

后端面試問題收集以及答案精簡版

思路 不要問什么答什么 要學會擴充 比如問你go map的原理 map 是什么 數據結構&#xff0c;字典&#xff0c;k/v 結構map的應用場景有哪些 快速查找、計數器、配置管理、去重、緩存實現map有哪些限制 無序性、非線程安全的讀寫map的key的訪問 v: mp[key] v,ok : mp[key] for…

MicroPython 開發ESP32應用教程 之 I2S、INMP441音頻錄制、MAX98357A音頻播放、SD卡讀寫

本課程我們講解Micropython for ESP32 的i2s及其應用&#xff0c;比如INMP441音頻錄制、MAX98357A音頻播放等&#xff0c;還有SD卡的讀寫。 一、硬件準備 1、支持micropython的ESP32S3開發板 2、INMP441數字全向麥克風模塊 3、MAX98357A音頻播放模塊 4、SD卡模塊 5、面包板及…

UE5 物理模擬 與 觸發檢測

文章目錄 碰撞條件開啟模擬關閉模擬 多層級的MeshUE的BUG 觸發觸發條件 碰撞 條件 1必須有網格體組件 2網格體組件必須有網格&#xff0c;沒有網格雖然可以開啟物理模擬&#xff0c;但是不會有任何效果 注意開啟的模擬的網格體組件會計算自己和所有子網格的mesh范圍 3只有網格…

微信小程序 - swiper輪播圖

官方文檔&#xff1a;https://developers.weixin.qq.com/miniprogram/dev/component/swiper.html <swiper indicator-color"ivory" indicator-active-color"#d43c33" indicator-dots autoplay><swiper-item><image src"/images/banner…

深入探究C#官方MCP:開啟AI集成新時代

一、引言 在當今數字化時代&#xff0c;.NET 開發領域不斷演進&#xff0c;而 C# 官方 MCP&#xff08;Model Context Protocol&#xff0c;模型上下文協議&#xff09;的出現&#xff0c;無疑為開發者們帶來了全新的機遇與挑戰。隨著人工智能技術的迅猛發展&#xff0c;將 AI…

二分查找法

使用二分查找法的前提&#xff1a;&#xff08;1&#xff09;數組為有序數組. &#xff08;2&#xff09;數組中無重復元素. 二分的兩種寫法&#xff1a; 方法一&#xff1a;[left&#xff0c;right] class Solution { public:int search(vector<int>& nums, int …

HarmonyOS:頁面滾動時標題懸浮、背景漸變

一、需求場景 進入到app首頁或者分頁列表首頁時&#xff0c;隨著頁面滾動&#xff0c;分類tab要求固定懸浮在頂部。進入到app首頁、者分頁列表首頁、商品詳情頁時&#xff0c;頁面滾動時&#xff0c;頂部導航欄&#xff08;菜單、標題&#xff09;背景漸變。 二、相關技術知識點…

鯤鵬+昇騰部署集群管理軟件GPUStack,兩臺服務器搭建雙節點集群【實戰詳細踩坑篇】

前期說明 配置&#xff1a;2臺鯤鵬32C2 2Atlas300I duo&#xff0c;之前看網上文檔&#xff0c;目前GPUstack只支持910B芯片&#xff0c;想嘗試一下能不能310P也部署試試&#xff0c;畢竟華為的集群軟件要收費。 系統&#xff1a;openEuler22.03-LTS 驅動&#xff1a;24.1.rc…

React中 點擊事件寫法 的注意(this、箭頭函數)

目錄 ?1、錯誤寫法?&#xff1a;onClick{this.acceptAlls()} ?2、正確寫法?&#xff1a;onClick{this.acceptAlls}&#xff08;不帶括號&#xff09; 總結 方案1&#xff1a;構造函數綁定 方案2&#xff1a;箭頭函數包裝方法&#xff08;更簡潔&#xff09; 方案3&am…

【路由交換方向IE認證】BGP選路原則之Weight屬性

文章目錄 一、路由器BGP路由的處理過程控制平面和轉發平面選路工具 二、BGP的選路順序選路的前提選路順序 三、Wight屬性選路原則規則9與規則11的潛移默化使用Weight值進行選路直接更改Weight值進行選路配合使用route-map進行選路 四、BGP鄰居建立配置 一、路由器BGP路由的處理…

Missashe考研日記-day20

Missashe考研日記-day20 1 高數 學習時間&#xff1a;2h30min學習內容&#xff1a; 今天當然是刷題啦&#xff0c;做不等式的證明板塊的真題&#xff0c;證明題懂的都懂&#xff0c;難起來是真的一點思路都沒有&#xff0c;這個板塊還沒做完&#xff0c;做完再總結題型。 2…

了解JVM

一.JVM概述 1.JVM的作用 ?把字節碼編譯為機器碼去執行,負責把字節碼裝載到虛擬機中 ?現在的 JVM 不僅可以執行 java 字節碼文件,還可以執行其他語言編譯后的字節碼文件,是一個跨語言平臺 2.JVM的組成部分 類加載器&#xff08;ClassLoader&#xff09;運行時數據區&#x…

LeetCode LCR157 套餐內商品的排列順序

生成字符串的全部排列&#xff08;去重&#xff09;&#xff1a;從問題到解決方案的完整解析 問題背景 在編程和算法設計中&#xff0c;生成字符串的所有排列是一個經典問題。它不僅出現在算法競賽中&#xff0c;也在實際開發中有著廣泛的應用&#xff0c;比如生成所有可能的…

pgsql:關聯查詢union(并集)、except(差集)、intersect(交集)

pgsql:關聯查詢union(并集)、except(差集)、intersect(交集)_pgsql except-CSDN博客

微信小程序中使用ECharts 并且動態設置數據

項目下載地址 GitHub 地址 https://github.com/ecomfe/echarts-for-weixin 將當前文件夾里的內容拷貝到項目中 目錄&#xff1a; json: {"usingComponents": {"ec-canvas": "../components/ec-canvas/ec-canvas"} }wxml&#xff1a; <ec…

RV1126 人臉識別門禁系統解決方案

1. 方案簡介 本方案為類人臉門禁機的產品級解決方案,已為用戶構建一個帶調度框架的UI應用工程;準備好我司的easyeai-api鏈接調用;準備好UI的開發環境。具備低模塊耦合度的特點。其目的在于方便用戶快速拓展自定義的業務功能模塊,以及快速更換UI皮膚。 2. 快速上手 2.1 開…

深度學習ResNet模型提取影響特征

大家好&#xff0c;我是帶我去滑雪&#xff01; 影像組學作為近年來醫學影像分析領域的重要研究方向&#xff0c;致力于通過從醫學圖像中高通量提取大量定量特征&#xff0c;以輔助疾病診斷、分型、預后評估及治療反應預測。這些影像特征涵蓋了形狀、紋理、灰度統計及波形變換等…

DeepSeek 接入 Word 完整教程

一、前期準備 1.1 注冊并獲取 API 密鑰 訪問 DeepSeek 平臺&#xff1a; 打開瀏覽器&#xff0c;訪問 DeepSeek 官方網站&#xff08;或您使用的相應平臺&#xff09;。注冊并登錄您的賬戶。 創建 API 密鑰&#xff1a; 在用戶控制面板中&#xff0c;找到“API Keys”或“API…