react之state深入浅出

第二章 - 添加交互

把一系列 state 更新加入队列

设置组件 state 会把一次重新渲染加入队列。但有时你可能会希望在下次渲染加入队列之前对 state 的值执行多次操作。为此,了解 React 如何批量更新 state 会很有帮助。

react 会对state 更新进行批处理

在下面的示例中,你可能会认为点击 “+3” 按钮会使计数器递增三次,因为它调用了 setNumber(number + 1) 三次:

import { useState } from 'react';export default function Counter() {const [number, setNumber] = useState(0);return (<><h1>{number}</h1><button onClick={() => {setNumber(number + 1);setNumber(number + 1);setNumber(number + 1);}}>+3</button></>)
}

但是,你可能还记得上一节中的内容,每一次渲染的 state 值都是固定的,因此无论你调用多少次 setNumber(1),在第一次渲染的事件处理函数内部的 number 值总是 0

setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);

但是这里还有另一个影响因素需要讨论。React会等到事件处理函数中的所有代码都运行完毕在处理你的state更新。这就是为什么重新渲染只会发生在所有这些 setNumber() 调用之后的原因。

这可能会让你想起餐厅里帮你点菜的服务员。服务员不会在你说第一道菜的时候就跑到厨房!相反,他们会让你把菜点完,让你修改菜品,甚至会帮桌上的其他人点菜。

这让你可以更新多个state变量 – 甚至来自多个组件的state变量 – 而不会触发太多的重新渲染。但这也意味着只有在你的事件处理函数及其中任何代码执行完成之后,UI才会更新。这种特性也就是批处理,它会使你的react应用运行的更快。它还会帮你避免处理只更新了一部分state变量的令人困惑的"半成品"渲染。

react不会跨多个需要刻意触发的事件(如点击)进行批处理 – 每次点击都是单独处理的。请放心,React 只会在一般来说安全的情况下才进行批处理。这可以确保,例如,如果第一次点击按钮会禁用表单,那么第二次点击就不会再次提交它。

在下次渲染前多次更新同一个state

这是一个不常见的用例,但是如果你想在下次渲染之前多次更新同一个 state,你可以像 setNumber(n => n + 1) 这样传入一个根据队列中的前一个 state 计算下一个 state 的 函数,而不是像 setNumber(number + 1) 这样传入 下一个 state 值。这是一种告诉 React “用 state 值做某事”而不是仅仅替换它的方法。

现在尝试递增计数器:

import { useState } from 'react';export default function Counter() {const [number, setNumber] = useState(0);return (<><h1>{number}</h1><button onClick={() => {setNumber(n => n + 1);setNumber(n => n + 1);setNumber(n => n + 1);}}>+3</button></>)
}

在这里,n => n + 1 被称为 更新函数。当你将它传递给一个 state 设置函数时:

  1. React 会将此函数加入队列,以便在事件处理函数中的所有其他代码运行后进行处理。
  2. 在下一次渲染期间,React 会遍历队列并给你更新之后的最终 state。
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);

下面是 React 在执行事件处理函数时处理这几行代码的过程:

  1. setNumber(n => n + 1)n => n + 1 是一个函数。React 将它加入队列。
  2. setNumber(n => n + 1)n => n + 1 是一个函数。React 将它加入队列。
  3. setNumber(n => n + 1)n => n + 1 是一个函数。React 将它加入队列。

当你在下次渲染期间调用 useState 时,React会遍历队列。之前的 number state的值是0,所以这就是react作为参数n传递给第一个更新函数的值。然后react会获取你上一个更新函数的返回值,并将其作为n传递给下一个更新函数,以此类推

更新队列n返回值
n => n + 100 + 1 = 1
n => n + 111 + 1 = 2
n => n + 122 + 1 = 3

React 会保存 3 为最终结果并从 useState 中返回。React 会保存 3 为最终结果并从 useState 中返回。

如果你在替换 state 后更新state会发生什么

这个事件处理函数会怎么样?你认为 number 在下一次渲染中的值是什么?

<button onClick={() => {setNumber(number + 5);setNumber(n => n + 1);
}}>

这是事件处理函数告诉 React 要做的事情:

  1. setNumber(number + 5)number0,所以 setNumber(0 + 5)。React 将 “替换为 5 添加到其队列中。
  2. setNumber(n => n + 1)n => n + 1 是一个更新函数。 React 将 该函数 添加到其队列中。

在下一次渲染期间,React 会遍历 state 队列:

更新队列n返回值
“替换为 50(未使用)5
n => n + 155 + 1 = 6

React 会保存 6 为最终结果并从 useState 中返回。

如果你在更新state后替换state会发生什么

让我们再看一个例子。你认为 number 在下一次渲染中的值是什么?

<button onClick={() => {setNumber(number + 5);setNumber(n => n + 1);setNumber(42);
}}>

下是 React 在执行事件处理函数时处理这几行代码的过程:

  1. setNumber(number + 5)number0,所以 setNumber(0 + 5)。React 将 “替换为 5 添加到其队列中。
  2. setNumber(n => n + 1)n => n + 1 是一个更新函数。React 将该函数添加到其队列中。
  3. setNumber(42):React 将 “替换为 42 添加到其队列中。

在下一次渲染期间,React 会遍历 state 队列:

更新队列n返回值
“替换为 50(未使用)5
n => n + 155 + 1 = 6
“替换为 426(未使用)42

然后 React 会保存 42 为最终结果并从 useState 中返回。

总而言之,以下是你可以考虑传递给 setNumber state 设置函数的内容:

  • 一个更新函数(例如:n => n + 1)会被添加到队列中。
  • 任何其他的值(例如:数字 5)会导致“替换为 5”被添加到队列中,已经在队列中的内容会被忽略。

事件处理函数执行完成后,React 将触发重新渲染。在重新渲染期间,React 将处理队列。更新函数会在渲染期间执行,因此 更新函数必须是 纯函数 并且只 返回 结果。不要尝试从它们内部设置 state 或者执行其他副作用。在严格模式下,React 会执行每个更新函数两次(但是丢弃第二个结果)以便帮助你发现错误。

命名惯例

通常可以通过相应 state 变量的第一个字母来命名更新函数的参数:

setEnabled(e => !e);setLastName(ln => ln.reverse());setFriendCount(fc => fc * 2);
摘要
  • 设置 state 不会更改现有渲染中的变量,但会请求一次新的渲染。
  • React 会在事件处理函数执行完成之后处理 state 更新。这被称为批处理。
  • 要在一个事件中多次更新某些 state,你可以使用 setNumber(n => n + 1) 更新函数。

更新 state 中的对象

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

什么是mutation

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

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

到目前为止,你已经尝试过在state中存放数字,字符串和布尔值,这些类型的值在JavaScript中是不可变的,这意味着它们不能被改变或是只读的。你可以通过替换它们的值以触发一次重新渲染。

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

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

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

position.x = 5;

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

将state视为只读的

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

在下面的例子中,我们用一个存放在 state 中的对象来表示指针当前的位置。当你在预览区触摸或移动光标时,红色的点本应移动。但是实际上红点仍停留在原处:

import { useState } from 'react';
export default function MovingDot() {const [position, setPosition] = useState({x: 0,y: 0});return (<divonPointerMove={e => {position.x = e.clientX;position.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>);
}

问题出在下面这段代码中。

onPointerMove={e => {position.x = e.clientX;position.y = e.clientY;
}}

这段代码直接修改了上一次渲染中 分配给 position的对象。但是因为并没有使用state设置函数,react并不知道对象已更改。所以react没有做出任何响应。这就像在吃完饭后才尝试去改变要点的菜一样。虽然在一些情况下,直接修改state可能是有效的,但我们并不推荐这么做。你应该把在渲染过程中可以访问到的state视为只读的。

在这种情况下,为了真正地触发一次重新渲染,你需要创建一个新对象并把它传递给state的设置函数。

onPointerMove={e => {setPosition({x: e.clientX,y: e.clientY});
}}

通过使用 setPosition,你在告诉 React:

  • 使用这个新的对象替换 position 的值
  • 然后再次渲染这个组件

现在你可以看到,当你在预览区触摸或移动光标时,红点会跟随着你的指针移动:

深入探讨 - 局部mutation是可以接受的

像这样的代码是有问题的,因为它改变了 state 中现有的对象:

position.x = e.clientX;position.y = e.clientY;

但是像这样的代码就 没有任何问题,因为你改变的是你刚刚创建的一个新的对象:

const nextPosition = {};nextPosition.x = e.clientX;nextPosition.y = e.clientY;setPosition(nextPosition);

事实上,它完全等同于下面这种写法:

setPosition({x: e.clientX,y: e.clientY});

只有当你改变已经处于 state 中的 现有 对象时,mutation 才会成为问题。而修改一个你刚刚创建的对象就不会出现任何问题,因为 还没有其他的代码引用它。改变它并不会意外地影响到依赖它的东西。这叫做“局部 mutation”。你甚至可以 在渲染的过程中 进行“局部 mutation”的操作。这种操作既便捷又没有任何问题!

使用展开语法复制对象

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

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

import { useState } from 'react';export default function Form() {const [person, setPerson] = useState({firstName: 'Barbara',lastName: 'Hepworth',email: 'bhepworth@sculpture.com'});function handleFirstNameChange(e) {person.firstName = e.target.value;}function handleLastNameChange(e) {person.lastName = e.target.value;}function handleEmailChange(e) {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></>);
}

例如,下面这行代码修改了上一次渲染中的state:

person.firstName = e.target.value;

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

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

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

可以看到,你并没有为每个输入框单独声明一个 state。对于大型表单,将所有数据都存放在同一个对象中是非常方便的——前提是你能够正确地更新它!

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

深入探讨 - 使用一个事件处理函数来更新多个字段

你也可以在对象定义中使用 [] 括号来实现属性的动态命名。下面是同一个例子,但它使用了一个事件处理函数而不是三个:

 function handleChange(e) {setPerson({...person,[e.target.name]: e.target.value});}

在这里,e.target.name 引用了 <input> 这个 DOM 元素的 name 属性。

更新一个嵌套对象

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

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!}
});

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

深入探讨 - 对象并非是真正嵌套的

下面这个对象从代码上来看是“嵌套”的:

let obj = {name: 'Niki de Saint Phalle',artwork: {title: 'Blue Nana',city: 'Hamburg',image: 'https://i.imgur.com/Sd1AgUOm.jpg',}};

然而,当我们思考对象的特性时,嵌套并不是一个非常准确的方式。当这段代码运行时,不存在嵌套的对象。你实际上看到的是两个不同的对象:

let obj1 = {title: 'Blue Nana',city: 'Hamburg',image: 'https://i.imgur.com/Sd1AgUOm.jpg',
};let obj2 = {name: 'Niki de Saint Phalle',artwork: obj1
};

对象 obj1 并不处于 obj2 的“内部”。例如,下面的代码中,obj3 中的属性也可以指向 obj1

let obj1 = {title: 'Blue Nana',city: 'Hamburg',image: 'https://i.imgur.com/Sd1AgUOm.jpg',};let obj2 = {name: 'Niki de Saint Phalle',artwork: obj1};let obj3 = {name: 'Copycat',artwork: obj1};

如果你直接修改 obj3.artwork.city,就会同时影响 obj2.artwork.cityobj1.city。这是因为 obj3.artworkobj2.artworkobj1 都指向同一个对象。当你用“嵌套”的方式看待对象时,很难看出这一点。相反,它们是相互独立的对象,只不过是用属性“指向”彼此而已。

使用 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 { useImmer } from 'use-immer';export default function Form() {const [person, updatePerson] = useImmer({name: 'Niki de Saint Phalle',artwork: {title: 'Blue Nana',city: 'Hamburg',image: 'https://i.imgur.com/Sd1AgUOm.jpg',}});function handleNameChange(e) {updatePerson(draft => {draft.name = e.target.value;});}function handleTitleChange(e) {updatePerson(draft => {draft.artwork.title = e.target.value;});}function handleCityChange(e) {updatePerson(draft => {draft.artwork.city = e.target.value;});}function handleImageChange(e) {updatePerson(draft => {draft.artwork.image = e.target.value;});}return (<><label>Name:<inputvalue={person.name}onChange={handleNameChange}/></label><label>Title:<inputvalue={person.artwork.title}onChange={handleTitleChange}/></label><label>City:<inputvalue={person.artwork.city}onChange={handleCityChange}/></label><label>Image:<inputvalue={person.artwork.image}onChange={handleImageChange}/></label><p><i>{person.artwork.title}</i>{' by '}{person.name}<br />(located in {person.artwork.city})</p><img src={person.artwork.image} alt={person.artwork.title}/></>);
}

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

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

更新state中的数组

数组是另外一种可以存储在state中的JavaScript对象,他虽然是可变的,但是却应该被视为不可变。同对象一样,当你想要更新存储于 state 中的数组时,你需要创建一个新的数组 (或者创建一份已有数组的拷贝值),并使用新数组设置state。

在没有mutation的前提下更新数组

在JavaScript中,数组只是另一种对象。同对象一样,你需要将react state 中的数组视为只读的。 这意味这你不应该使用类似于 arr[0] = 'bird' 这样的方式来重新分配数组中的元素,也不应该使用会直接修改原始数组的方法,例如 push()pop()

相反,每次要更新一个数组时,你需要把一个的数组传入 state 的 setting 方法中。为此,你可以通过使用像 filter()map() 这样不会直接修改原始值的方法,从原始数组生成一个新的数组。然后你就可以将 state 设置为这个新生成的数组。

下面是常见数组操作的参考表。当你操作 React state 中的数组时,你需要避免使用左列的方法,而首选右列的方法:

避免使用 (会改变原始数组)推荐使用 (会返回一个新数组)
添加元素pushunshiftconcat[...arr] 展开语法(例子)
删除元素popshiftsplicefilterslice(例子)
替换元素splicearr[i] = ... 赋值map(例子)
排序reversesort先将数组复制一份(例子)

或者,你可以使用 Immer ,这样你便可以使用表格中的所有方法了。

陷阱

不幸的是,虽然 slicesplice 的名字相似,但作用却迥然不同:

  • slice 让你可以拷贝数组或是数组的一部分。
  • splice 会直接修改 原始数组(插入或者删除元素)。

在 React 中,更多情况下你会使用 slice(没有 p !),因为你不想改变 state 中的对象或数组。更新对象这一章节解释了什么是 mutation,以及为什么不推荐在 state 里这样做。

向数组中添加元素

push() 会直接修改原始数组,而你不希望这样:

import { useState } from 'react';let nextId = 0;export default function List() {const [name, setName] = useState('');const [artists, setArtists] = useState([]);return (<><h1>振奋人心的雕塑家们:</h1><inputvalue={name}onChange={e => setName(e.target.value)}/><button onClick={() => {artists.push({id: nextId++,name: name,});}}>添加</button><ul>{artists.map(artist => (<li key={artist.id}>{artist.name}</li>))}</ul></>);
}

相反,你应该创建一个新数组,其包含了原始数组的所有元素 以及一个在末尾的新元素。这可以通过很多种方法实现,最简单的一种就是使用... 数组展开 语法。

setArtists( // 替换 state[ // 是通过传入一个新数组实现的...artists, // 新数组包含原数组的所有元素{ id: nextId++, name: name } // 并在末尾添加了一个新的元素]
);

数组展开运算符还允许你把新添加的元素放在原始的...artists 之前:

setArtists([{ id: nextId++, name: name },...artists // 将原数组中的元素放在末尾
]);

这样一来,展开操作就可以完成 push()unshift() 的工作,将新元素添加到数组的末尾和开头。你可以在上面的 sandbox 中尝试一下!

从数组中删除元素

从数组中删除一个元素最简单的方法就是将它过滤出去。换句话说,你需要生成一个不包含该元素的新数组。这可以通过filter方法实现,例如:

import { useState } from 'react';let initialArtists = [{ id: 0, name: 'Marta Colvin Andrade' },{ id: 1, name: 'Lamidi Olonade Fakeye'},{ id: 2, name: 'Louise Nevelson'},
];export default function List() {const [artists, setArtists] = useState(initialArtists);return (<><h1>振奋人心的雕塑家们:</h1><ul>{artists.map(artist => (<li key={artist.id}>{artist.name}{' '}<button onClick={() => {setArtists(artists.filter(a =>a.id !== artist.id));}}>删除</button></li>))}</ul></>);
}

点击删除按钮几次,并且查看按钮处理点击事件的代码。

setArtists(artists.filter(a => a.id !== artist.id)
);

这里,artists.filter(s => s.id !== artist.id) 表示“创建一个新的数组,该数组由那些 ID 与 artists.id 不同的 artists 组成”。换句话说,每个 artist 的“删除”按钮会把 那一个 artist 从原始数组中过滤掉,并使用过滤后的数组再次进行渲染。注意,filter 并不会改变原始数组。

转换数组

如果你想改变数组中的某些或全部元素,你可以使用map( 创建一个新数组。你传入map的函数决定了要根据每个函数的值或索引对元素做何处理。

在下面的例子中,一个数组记录了两个圆形和一个正方形的坐标。当你点击按钮时,仅有两个圆形会向下移动100像素。这是通过使用 map() 生成一个新数组实现的。

import { useState } from 'react';let initialShapes = [{ id: 0, type: 'circle', x: 50, y: 100 },{ id: 1, type: 'square', x: 150, y: 100 },{ id: 2, type: 'circle', x: 250, y: 100 },
];export default function ShapeEditor() {const [shapes, setShapes] = useState(initialShapes);function handleClick() {const nextShapes = shapes.map(shape => {if (shape.type === 'square') {// 不作改变return shape;} else {// 返回一个新的圆形,位置在下方 50px 处return {...shape,y: shape.y + 50,};}});// 使用新的数组进行重渲染setShapes(nextShapes);}return (<><button onClick={handleClick}>所有圆形向下移动!</button>{shapes.map(shape => (<divkey={shape.id}style={{background: 'purple',position: 'absolute',left: shape.x,top: shape.y,borderRadius:shape.type === 'circle'? '50%' : '',width: 20,height: 20,}} />))}</>);
}

替换数组中的元素

想要替换数组中一个或多个元素是非常常见的。类似 arr[0] = 'bird' 这样的赋值语句会直接修改原始数组,所以在这种情况下,你也应该使用 map

要替换一个元素,请使用 map 创建一个新数组。在你的 map 回调里,第二个参数是元素的索引。使用索引来判断最终是返回原始的元素(即回调的第一个参数)还是替换成其他值:

import { useState } from 'react';let initialCounters = [0, 0, 0
];export default function CounterList() {const [counters, setCounters] = useState(initialCounters);function handleIncrementClick(index) {const nextCounters = counters.map((c, i) => {if (i === index) {// 递增被点击的计数器数值return c + 1;} else {// 其余部分不发生变化return c;}});setCounters(nextCounters);}return (<ul>{counters.map((counter, i) => (<li key={i}>{counter}<button onClick={() => {handleIncrementClick(i);}}>+1</button></li>))}</ul>);
}

向数组中插入元素

有时,你也许想向数组特定位置插入一个元素,这个位置既不在数组开头,也不在末尾。为此,你可以将数组展开运算符 ...slice() 方法一起使用。slice() 方法让你从数组中切出“一片”。为了将元素插入数组,你需要先展开原数组在插入点之前的切片,然后插入新元素,最后展开原数组中剩下的部分。

下面的例子中,插入按钮总是会将元素插入到数组中索引为 1 的位置。

import { useState } from 'react';let nextId = 3;
const initialArtists = [{ id: 0, name: 'Marta Colvin Andrade' },{ id: 1, name: 'Lamidi Olonade Fakeye'},{ id: 2, name: 'Louise Nevelson'},
];export default function List() {const [name, setName] = useState('');const [artists, setArtists] = useState(initialArtists);function handleClick() {const insertAt = 1; // 可能是任何索引const nextArtists = [// 插入点之前的元素:...artists.slice(0, insertAt),// 新的元素:{ id: nextId++, name: name },// 插入点之后的元素:...artists.slice(insertAt)];setArtists(nextArtists);setName('');}return (<><h1>振奋人心的雕塑家们:</h1><inputvalue={name}onChange={e => setName(e.target.value)}/><button onClick={handleClick}>插入</button><ul>{artists.map(artist => (<li key={artist.id}>{artist.name}</li>))}</ul></>);
}

其他改变数组的情况

总会有一些事,是你仅仅依靠展开运算符和 map() 或者 filter() 等不会直接修改原值的方法所无法做到的。例如,你可能想翻转数组,或是对数组排序。而 JavaScript 中的 reverse()sort() 方法会改变原数组,所以你无法直接使用它们。

**然而,你可以先拷贝这个数组,再改变这个拷贝后的值。**例如:

import { useState } from 'react';const initialList = [{ id: 0, title: 'Big Bellies' },{ id: 1, title: 'Lunar Landscape' },{ id: 2, title: 'Terracotta Army' },
];export default function List() {const [list, setList] = useState(initialList);function handleClick() {const nextList = [...list];nextList.reverse();setList(nextList);}return (<><button onClick={handleClick}>翻转</button><ul>{list.map(artwork => (<li key={artwork.id}>{artwork.title}</li>))}</ul></>);
}

在这段代码中,你先使用 [...list] 展开运算符创建了一份数组的拷贝值。当你有了这个拷贝值后,你就可以使用像 nextList.reverse()nextList.sort() 这样直接修改原数组的方法。你甚至可以通过 nextList[0] = "something" 这样的方式对数组中的特定元素进行赋值。

然而,即使你拷贝了数组,你还是不能直接修改其内部的元素。这是因为数组的拷贝是浅拷贝——新的数组中依然保留了与原始数组相同的元素。因此,如果你修改了拷贝数组内部的某个对象,其实你正在直接修改当前的 state。举个例子,像下面的代码就会带来问题。

const nextList = [...list];
nextList[0].seen = true; // 问题:直接修改了 list[0] 的值
setList(nextList);

虽然 nextListlist 是两个不同的数组,nextList[0]list[0] 却指向了同一个对象。因此,通过改变 nextList[0].seenlist[0].seen 的值也被改变了。这是一种 state 的 mutation 操作,你应该避免这么做!你可以用类似于 更新嵌套的 JavaScript 对象 的方式解决这个问题——拷贝想要修改的特定元素,而不是直接修改它。下面是具体的操作。

更新数组内部的对象

对象并不是 真的 位于数组“内部”。可能他们在代码中看起来像是在数组“内部”,但其实数组中的每个对象都是这个数组“指向”的一个存储于其它位置的值。这就是当你在处理类似 list[0] 这样的嵌套字段时需要格外小心的原因。其他人的艺术品清单可能指向了数组的同一个元素!

当你更新一个嵌套的 state 时,你需要从想要更新的地方创建拷贝值,一直这样,直到顶层。 让我们看一下这该怎么做。

在下面的例子中,两个不同的艺术品清单有着相同的初始 state。他们本应该互不影响,但是因为一次 mutation,他们的 state 被意外地共享了,勾选一个清单中的事项会影响另外一个清单:

import { useState } from 'react';let nextId = 3;
const initialList = [{ id: 0, title: 'Big Bellies', seen: false },{ id: 1, title: 'Lunar Landscape', seen: false },{ id: 2, title: 'Terracotta Army', seen: true },
];export default function BucketList() {const [myList, setMyList] = useState(initialList);const [yourList, setYourList] = useState(initialList);function handleToggleMyList(artworkId, nextSeen) {const myNextList = [...myList];const artwork = myNextList.find(a => a.id === artworkId);artwork.seen = nextSeen;setMyList(myNextList);}function handleToggleYourList(artworkId, nextSeen) {const yourNextList = [...yourList];const artwork = yourNextList.find(a => a.id === artworkId);artwork.seen = nextSeen;setYourList(yourNextList);}return (<><h1>艺术愿望清单</h1><h2>我想看的艺术清单:</h2><ItemListartworks={myList}onToggle={handleToggleMyList} /><h2>你想看的艺术清单:</h2><ItemListartworks={yourList}onToggle={handleToggleYourList} /></>);
}function ItemList({ artworks, onToggle }) {return (<ul>{artworks.map(artwork => (<li key={artwork.id}><label><inputtype="checkbox"checked={artwork.seen}onChange={e => {onToggle(artwork.id,e.target.checked);}}/>{artwork.title}</label></li>))}</ul>);
}

问题出在下面这段代码中:

const myNextList = [...myList];const artwork = myNextList.find(a => a.id === artworkId);artwork.seen = nextSeen; // 问题:直接修改了已有的元素setMyList(myNextList);

虽然 myNextList 这个数组是新的,但是其内部的元素本身与原数组 myList 是相同的。因此,修改 artwork.seen,其实是在修改原始的 artwork 对象。而这个 artwork 对象也被 yourList 使用,这样就带来了 bug。这样的 bug 可能难以想到,但好在如果你避免直接修改 state,它们就会消失。

你可以使用 map 在没有 mutation 的前提下将一个旧的元素替换成更新的版本。

setMyList(myList.map(artwork => {if (artwork.id === artworkId) {// 创建包含变更的*新*对象return { ...artwork, seen: nextSeen };} else {// 没有变更return artwork;}
}));

此处的 ... 是一个对象展开语法,被用来创建一个对象的拷贝.

通过这种方式,没有任何现有的 state 中的元素会被改变,bug 也就被修复了。

通常来讲,你应该只直接修改你刚刚创建的对象。如果你正在插入一个的 artwork,你可以修改它,但是如果你想要改变的是 state 中已经存在的东西,你就需要先拷贝一份了。

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

在没有 mutation 的前提下更新嵌套数组可能会变得有点重复。就像对对象一样:

  • 通常情况下,你应该不需要更新处于非常深层级的 state 。如果你有此类需求,你或许需要调整一下数据的结构,让数据变得扁平一些。
  • 如果你不想改变 state 的数据结构,你也许会更喜欢使用 Immer ,它让你可以继续使用方便的,但会直接修改原值的语法,并负责为你生成拷贝值。

下面是我们用 Immer 来重写的艺术愿望清单的例子:

import { useState } from 'react';
import { useImmer } from 'use-immer';let nextId = 3;
const initialList = [{ id: 0, title: 'Big Bellies', seen: false },{ id: 1, title: 'Lunar Landscape', seen: false },{ id: 2, title: 'Terracotta Army', seen: true },
];export default function BucketList() {const [myList, updateMyList] = useImmer(initialList);const [yourList, updateYourList] = useImmer(initialList);function handleToggleMyList(id, nextSeen) {updateMyList(draft => {const artwork = draft.find(a =>a.id === id);artwork.seen = nextSeen;});}function handleToggleYourList(artworkId, nextSeen) {updateYourList(draft => {const artwork = draft.find(a =>a.id === artworkId);artwork.seen = nextSeen;});}return (<><h1>艺术愿望清单</h1><h2>我想看的艺术清单:</h2><ItemListartworks={myList}onToggle={handleToggleMyList} /><h2>你想看的艺术清单:</h2><ItemListartworks={yourList}onToggle={handleToggleYourList} /></>);
}function ItemList({ artworks, onToggle }) {return (<ul>{artworks.map(artwork => (<li key={artwork.id}><label><inputtype="checkbox"checked={artwork.seen}onChange={e => {onToggle(artwork.id,e.target.checked);}}/>{artwork.title}</label></li>))}</ul>);
}

请注意当使用 Immer 时,类似 artwork.seen = nextSeen 这种会产生 mutation 的语法不会再有任何问题了:

updateMyTodos(draft => {const artwork = draft.find(a => a.id === artworkId);artwork.seen = nextSeen;});

这是因为你并不是在直接修改原始的 state,而是在修改 Immer 提供的一个特殊的 draft 对象。同理,你也可以为 draft 的内容使用 push()pop() 这些会直接修改原值的方法。

在幕后,Immer 总是会根据你对 draft 的修改来从头开始构建下一个 state。这使得你的事件处理程序非常的简洁,同时也不会直接修改 state。

摘要
  • 你可以把数组放入 state 中,但你不应该直接修改它。
  • 不要直接修改数组,而是创建它的一份 新的 拷贝,然后使用新的数组来更新它的状态。
  • 你可以使用 [...arr, newItem] 这样的数组展开语法来向数组中添加元素。
  • 你可以使用 filter()map() 来创建一个经过过滤或者变换的数组。
  • 你可以使用 Immer 来保持代码简洁。

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

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

相关文章

论文阅读之MMSD2.0: Towards a Reliable Multi-modal Sarcasm Detection System

文章目录 论文地址主要内容主要贡献模型图技术细节数据集改进多视图CLIP框架文本视图图像视图图像-文本交互视图 实验结果 论文地址 https://arxiv.org/pdf/2307.07135 主要内容 这篇文章介绍了一个名为MMSD2.0的多模态讽刺检测系统的构建&#xff0c;旨在提高现有讽刺检测系…

【无人机路径规划】基于A算法求解无人机三维路径规划问题matlab源码

无人机路径规划 无人机路径规划是指为无人机制定一个最佳的飞行路径&#xff0c;使其能够有效地完成特定任务或达到目标位置。路径规划通常涉及以下几个方面&#xff1a; 地图和环境建模&#xff1a;首先需要对飞行区域进行地图建模&#xff0c;包括地形、障碍物、限制区域等…

B+tree - B+树深度解析+C语言实现+opencv绘图助解

Btree - B树深度解析C语言实现opencv绘图助解 1. 概述2. Btree介绍3. Btree算法实现3.1 插入分裂 3.2 删除向右借位&#xff08;左旋&#xff09;向左借位&#xff08;右旋&#xff09;合并 3.3 查询和遍历3.3.1 查询3.3.2 遍历 3.4 优化优化1(匀key)优化2(升级key)优化3(拓展兄…

vue3 vite 路由去中心化(modules文件夹自动导入router)

通过路由去中心化可实现多人写作开发&#xff0c;不怕文件不停修改导致的冲突&#xff0c;modules中的文件可自动导入到index.js中 // 自动导入模块 const files import.meta.globEager(./modules/**.js); const modules {} for (const key in files) {modules[key.replace…

Android 开发工具使用

c调试 在NDK调试的时候&#xff0c;如果找不到 符号的话&#xff0c;我们可以在调试配置中添加符号地址的全路径一直到根目录&#xff1a;&#xff0c;xxx/armeabi-v7a&#xff1a; You must point the symbol search paths at the obj/local/ directory. This is also not a …

【Vue】如何使用Webpack实现打包操作

一、Webpack介绍 Webpack最主要的作用就是打包操作&#xff0c;由两个核心部分构成分别是“出口”与“入口”。wbepack是现在比较热门的打包工具了&#xff0c;它可以将许多松散耦合的模块按照依赖和规则打包成符合生产环境部署的前端资源。说的直白一点&#xff0c;通过webpac…

基于单片机的配电网故障自适应监控系统设计

摘 要: 为解决配电网收集故障参数的误差补偿不足,谐波测量数据准确度较低的问题,提出基于单片机的配电网故障自适应监控 系统设计。在硬件中添加电平转换模块,利用数据储存器进行参数处理;使用SCADA软件实现数据的采集以及故障警告。 进行实验得到结果:设计系统对相关参数…

Linux 内核深入理解 - 绪论

目录 多用户系统 进程 内核体系架构 文件系统概述 Base 硬链接和软链接 Unix文件类型 文件描述符与索引节点 文件操作的系统调用 Unix内核简述 进程的实现 可重入内核 进程地址空间 同步和临界区 信号与进程之间的通信 进程管理 内存管理 虚拟内存 随机访问存…

漏洞端到端管理小总结

漏洞端到端管理最佳实践涵盖了从漏洞的发现、分析、修复到监控的整个过程&#xff0c;确保组织能够及时发现并应对安全威胁。以下是一些建议的最佳实践&#xff1a; 发现与评估&#xff1a; 资产识别与分类&#xff1a;对组织的所有网络资产进行彻底清查&#xff0c;包括但不限…

redission原理笔记

加锁成功的线程&#xff0c;将UUID和线程id和key绑定&#xff0c; 加锁成功后&#xff0c;内部有一个看门狗机制&#xff0c;每隔十秒看下当前线程是否还持有锁&#xff0c;延长生存时间。 没有获取锁的就一直自旋等待&#xff0c;直到超时。 如果redis是主从同步的&#xff0…

MySQL__深度分页问题

文章目录 &#x1f60a; 作者&#xff1a;Lion J &#x1f496; 主页&#xff1a; https://blog.csdn.net/weixin_69252724 &#x1f389; 主题&#xff1a; MySQL__深度分页问题&#xff09; ⏱️ 创作时间&#xff1a;2024年04月27日 ———————————————— …

自动驾驶新书“五一”节马上上市了

我和杨子江教授合写的《自动驾驶系统开发》终于在清华大学出版社三校稿之后即将在五一节后出版。 清华大学汽车学院的李克强教授和工程院院士撰写了序言。 该书得到了唯一华人图灵奖获得者姚期智院士、西安交大管晓宏教授和科学院院士以及杨强教授和院士等的推荐&#xff0c;…

不使用加减运算符实现整数加和减

文章目录 进位 进位 加粗 最近想出了不使用运算符实现加与减 首先按位与找出的是需不需要进位 按位与是两边同时为1,则为1,那么如果两边同时为1的话,是不是就该进位?所以我们用按位与来判断是否需要进位 然后再按位异或找出不同的位数 按位异或是两边不相等,也就是1 和 0的时…

[每周一更]-(第94期):认识英伟达显卡

英伟达显卡&#xff1a;引领图形计算的领先者&#xff0c;显卡也常称为GPU&#xff08;图形处理器 Graphics processing unit&#xff09;&#xff0c;是一种专门在个人电脑、工作站、游戏机和一些移动设备&#xff08;如平板电脑、智能手机等&#xff09;上执行绘图运算工作的…

CVPR2022 ACmix 注意力模块 | On the Integration of Self-Attention and Convolution

论文名称&#xff1a;《On the Integration of Self-Attention and Convolution》 论文地址&#xff1a;2111.14556 (arxiv.org) 卷积和自注意力是两种强大的表示学习技术&#xff0c;通常被认为是两种截然不同的并列方法。在本文中&#xff0c;我们展示了它们之间存在一种强烈…

排序试题解析(二)

8.4.3 01.在以下排序算法中&#xff0c;每次从未排序的记录中选取最小关键字的记录&#xff0c;加入已排序记录的 末尾&#xff0c;该排序算法是( A ). A.简单选择排序 B.冒泡排序 C.堆排序 D.直接插入排序 02&#xff0e;简单选择排序算法的比较次数和移动次数分别为( C )。…

微信小程序手写文件解决日期少一天且格式无法切割问题

编译环境 微信开发者工具 问题 在小程序中无法实现对日期的切割&#xff0c;并且可能会出现日期少一天的问题&#xff0c;这个问题可以由后端进行解决&#xff0c;也可以前端&#xff0c;这里用了前端新建一个wxs转换文件进行解决。 比如数据库中的数据是2024-03-02… 但是返…

js动态设置css主题(Style-setProperty)

hex颜色转RGB hex2Rgb(str) {str str.replace("#", "");const hxs str.match(/../g);for (let index 0; index < 3; index) hxs[index] parseInt(hxs[index], 16);return hxs; } RGB转HXS rgb2hex(r,g,b){const hexs [r.toString(16), g.toString…

UE5蓝图 函数勾选线程安全的意义,我在动画蓝图状态机中调用了函数(gpt答复分享)

在Unreal Engine中&#xff0c;蓝图函数的“线程安全”选项通常用于确定该函数是否可以安全地在多线程环境下调用。线程安全意味着函数在执行时不会导致数据竞争&#xff0c;状态错误&#xff0c;或其他并发问题。如果一个函数是线程安全的&#xff0c;它就可以在不同的线程中同…

【小沐学Java】VSCode搭建Java开发环境

文章目录 1、简介2、安装VSCode2.1 简介2.2 安装 3、安装Java SDK3.1 简介3.2 安装3.3 配置 4、安装插件Java Extension Pack4.1 简介4.2 安装4.3 配置 结语 1、简介 2、安装VSCode 2.1 简介 Visual Studio Code 是一个轻量级但功能强大的源代码编辑器&#xff0c;可在桌面上…