Обновление объектов в состоянии
Состояние может хранить любые типы значений JavaScript, включая объекты. Но вы не должны напрямую изменять объекты, хранящиеся в состоянии React. Вместо этого, при необходимости обновления объекта, необходимо создать новый объект (или скопировать существующий), а затем установить состояние, используя эту копию.
Вы узнаете
- Как правильно обновить объект в состоянии React
- Как обновить вложенный объект без его изменения
- Что такое неизменяемость и как ее не нарушить
- Как сделать копирование объектов менее повторяющимся с помощью Immer
Что такое мутация?
Вы можете хранить любые типы значений JavaScript в состоянии.
const [x, setX] = useState(0); До сих пор вы работали с числами, строками и булевыми значениями. Эти типы значений JavaScript являются «неизменяемыми», то есть неизменяемыми или «только для чтения». Вы можете запустить повторную отрисовку, чтобы заменить значение:
setX(5); Состояние изменилось с 0 на 5, но само число 0 не изменилось. Невозможно внести какие-либо изменения в встроенные примитивные значения, такие как числа, строки и булевы значения в JavaScript.
Теперь рассмотрим объект в состоянии:
const [position, setPosition] = useState({ x: 0, y: 0 }); Технически, возможно изменить содержимое самого объекта. Это называется мутацией:
position.x = 5; Однако, хотя объекты в состоянии React технически изменяемы, вы должны рассматривать их как неизменяемые — как числа, булевы значения и строки. Вместо того, чтобы изменять их, вы всегда должны их заменять.
Обращаться к состоянию как к неизменяемому
Другими словами, вы должны обращаться к любому объекту JavaScript, который вы помещаете в состояние, как к неизменяемому.
В этом примере в состоянии хранится объект для представления текущего положения указателя. Красная точка должна перемещаться, когда вы касаетесь или перемещаете курсор над областью предварительного просмотра. Но точка остается в исходном положении:
import { useState } from 'react'; export default function MovingDot() { const [position, setPosition] = useState({ x: 0, y: 0 }); return ( <div onPointerMove={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, из предыдущей отрисовки. Но без использования функции установки состояния React не знает, что объект изменился. Поэтому React ничего не делает в ответ. Это как пытаться изменить порядок после того, как вы уже съели блюдо. Хотя мутация состояния может работать в некоторых случаях, мы не рекомендуем ее. Вы должны рассматривать значение состояния, к которому у вас есть доступ в отрисовке, как неизменяемое.
Чтобы фактически запустить повторную отрисовку в этом случае, создайте новый объект и передайте его в функцию установки состояния:
onPointerMove={e => {
setPosition({
x: e.clientX,
y: e.clientY
});
}} С помощью setPosition, вы говорите React:
- Заменить
positionэтим новым объектом - И перерисовать этот компонент снова
Обратите внимание, как красная точка теперь следует за вашим указателем, когда вы касаетесь или наводите курсор на область предварительного просмотра:
import { useState } from 'react'; export default function MovingDot() { 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> ); }
Подробный разбор
Локальная мутация допустима
Код подобного вида является проблемой, поскольку он изменяет существующий объект в состоянии:
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
});
Мутация является проблемой только тогда, когда вы изменяете существующие объекты, которые уже находятся в состоянии. Изменение объекта, который вы только что создали, допустимо, потому что никакой другой код на него еще не ссылается. Изменение его не повлияет случайно на что-либо, что зависит от него. Это называется «локальной мутацией». Вы можете даже делать локальную мутацию во время отрисовки. Очень удобно и совершенно приемлемо!
Копирование объектов с помощью синтаксиса spread
В предыдущем примере объект position всегда создается заново из текущего положения курсора. Но часто вам нужно включить существующие данные в новый объект, который вы создаете. Например, вы можете обновить только одно поле в форме, но сохранить предыдущие значения для всех других полей.
Эти поля ввода не работают, потому что обработчики onChange изменяют состояние:
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: <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> </> ); }
Например, эта строка изменяет состояние из предыдущей отрисовки:
person.firstName = e.target.value; Надежный способ получить желаемое поведение — создать новый объект и передать его в setPerson. Но здесь вы также хотите скопировать существующие данные в него, потому что изменилось только одно из полей:
setPerson({
firstName: e.target.value, // New first name from the input
lastName: person.lastName,
email: person.email
}); Вы можете использовать синтаксис ... распространения объектов, чтобы вам не нужно было копировать каждую собственность по отдельности.
setPerson({
...person, // Copy the old fields
firstName: e.target.value // But override this one
}); Теперь форма работает!
Обратите внимание, что вы не объявляли отдельной переменной состояния для каждого поля ввода. Для больших форм сохранение всех данных в группе в объекте очень удобно — при условии правильного обновления!
import { useState } from 'react'; export default function Form() { const [person, setPerson] = useState({ firstName: 'Barbara', lastName: 'Hepworth', email: 'bhepworth@sculpture.com' }); function handleFirstNameChange(e) { setPerson({ ...person, firstName: e.target.value }); } function handleLastNameChange(e) { setPerson({ ...person, lastName: e.target.value }); } function handleEmailChange(e) { setPerson({ ...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> </> ); }
Обратите внимание, что синтаксис ... spread является «поверхностным» — он копирует только вещи на одном уровне вглубь. Это делает его быстрым, но это также означает, что если вы хотите обновить вложенное свойство, вам придется использовать его более одного раза.
Подробный разбор
Использование одного обработчика событий для нескольких полей
Вы также можете использовать [ и ] фигурные скобки внутри определения объекта, чтобы указать свойство с динамическим именем. Вот тот же пример, но с одним обработчиком событий вместо трех различных:
import { useState } from 'react'; export default function Form() { const [person, setPerson] = useState({ firstName: 'Barbara', lastName: 'Hepworth', email: 'bhepworth@sculpture.com' }); function handleChange(e) { setPerson({ ...person, [e.target.name]: e.target.value }); } return ( <> <label> First name: <input name="firstName" value={person.firstName} onChange={handleChange} /> </label> <label> Last name: <input name="lastName" value={person.lastName} onChange={handleChange} /> </label> <label> Email: <input name="email" value={person.email} onChange={handleChange} /> </label> <p> {person.firstName}{' '} {person.lastName}{' '} ({person.email}) </p> </> ); }
Здесь e.target.name относится к свойству name, заданному элементу <input> DOM.
Обновление вложенного объекта
Рассмотрим структуру вложенного объекта, подобную этой:
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, очевидно, как это сделать с помощью мутации:
person.artwork.city = 'New Delhi'; Но в React вы обрабатываете состояние как неизменяемое! Для изменения city, вам сначала нужно создать новый объект artwork (предварительно заполненный данными из предыдущего), а затем создать новый объект person, который указывает на новый объект artwork:
const nextArtwork = { ...person.artwork, city: 'New Delhi' };
const nextPerson = { ...person, artwork: nextArtwork };
setPerson(nextPerson); Или, записанное как одно вызов функции:
setPerson({
...person, // Copy other fields
artwork: { // but replace the artwork
...person.artwork, // with the same one
city: 'New Delhi' // but in New Delhi!
}
}); Это немного многословно, но работает хорошо во многих случаях:
import { useState } from 'react'; export default function Form() { const [person, setPerson] = useState({ name: 'Niki de Saint Phalle', artwork: { title: 'Blue Nana', city: 'Hamburg', image: 'https://i.imgur.com/Sd1AgUOm.jpg', } }); function handleNameChange(e) { setPerson({ ...person, name: e.target.value }); } function handleTitleChange(e) { setPerson({ ...person, artwork: { ...person.artwork, title: e.target.value } }); } function handleCityChange(e) { setPerson({ ...person, artwork: { ...person.artwork, city: e.target.value } }); } function handleImageChange(e) { setPerson({ ...person, artwork: { ...person.artwork, image: e.target.value } }); } return ( <> <label> Name: <input value={person.name} onChange={handleNameChange} /> </label> <label> Title: <input value={person.artwork.title} onChange={handleTitleChange} /> </label> <label> City: <input value={person.artwork.city} onChange={handleCityChange} /> </label> <label> Image: <input value={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} /> </> ); }
Подробный разбор
Объекты на самом деле не вложены
Объект такого вида кажется «вложенным» в коде:
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.city, так и на obj1.city. Это потому, что obj3.artwork, obj2.artwork, и obj1 — один и тот же объект. Это трудно увидеть, когда вы представляете объекты как «вложенные». Вместо этого это отдельные объекты, «указывающие» друг на друга с помощью свойств.
Написание лаконичного логики обновления с помощью Immer
Если ваше состояние глубоко вложено, вы можете захотеть рассмотреть его выравнивание. Но если вы не хотите изменять структуру своего состояния, вы можете предпочесть сокращение для вложенных распространений. Immer — популярная библиотека, которая позволяет вам писать с удобным, но изменяющим синтаксисом, и которая позаботится о создании копий за вас. С помощью Immer код, который вы пишете, выглядит так, будто вы «нарушаете правила» и изменяете объект:
updatePerson(draft => {
draft.artwork.city = 'Lagos';
}); Но, в отличие от обычной мутации, он не перезаписывает предыдущее состояние!
Подробный разбор
Как работает Immer?
Объект, предоставляемый Immer, — это специальный тип объекта, называемый Proxy, который «записывает» то, что вы с ним делаете. Вот почему вы можете свободно изменять его сколько угодно! Под капотом Immer определяет, какие части draft были изменены, и создает совершенно новый объект, содержащий ваши правки.
Чтобы попробовать Immer:
- Запустите
npm install use-immerдля добавления Immer в качестве зависимости - Затем замените
import { useState } from 'react'наimport { useImmer } from 'use-immer'
Вот приведенный выше пример, преобразованный в Immer:
{ "dependencies": { "immer": "1.7.3", "react": "latest", "react-dom": "latest", "react-scripts": "latest", "use-immer": "0.5.1" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" }, "devDependencies": {} }
Обратите внимание, насколько более лаконичными стали обработчики событий. Вы можете смешивать useState и useImmer в одном компоненте столько, сколько вам нужно. Immer — отличный способ сохранить обработчики обновлений лаконичными, особенно если в вашем состоянии есть вложенность, а копирование объектов приводит к повторяющемуся коду.
Глубокое погружение
Почему изменение состояния не рекомендуется в React?
Есть несколько причин:
-
Отладка: Если вы используете
console.logи не изменяете состояние, ваши предыдущие логи не будут перезаписываться более поздними изменениями состояния. Так вы сможете четко увидеть, как состояние изменилось между рендерами. -
Оптимизации: Общие стратегии оптимизации React стратегии оптимизации полагаются на пропуск работы, если предыдущие свойства или состояние совпадают со следующими. Если вы никогда не изменяете состояние, очень быстро проверить, были ли изменения. Если
prevObj === obj, вы можете быть уверены, что ничего не могло измениться внутри него. - Новые возможности: Новые функции React, которые мы разрабатываем, полагаются на то, что состояние обращается как моментальный снимок. Если вы изменяете предыдущие версии состояния, это может помешать вам использовать новые функции.
- Изменение требований: Некоторые функции приложения, такие как реализация отмены/повторного выполнения, отображение истории изменений или предоставление пользователю возможности сбросить форму до предыдущих значений, проще реализовать, когда ничего не изменяется. Это потому, что вы можете хранить предыдущие копии состояния в памяти и повторно использовать их при необходимости. Если вы начинаете с изменяющего подхода, такие функции, как эта, могут быть сложнее добавить позже.
- Простая реализация: Поскольку React не полагается на изменения, ему не нужно ничего особенного делать с вашими объектами. Ему не нужно перехватывать их свойства, всегда оборачивать их в прокси или выполнять другие операции при инициализации, как делают многие «реактивные» решения. Именно поэтому React позволяет помещать в состояние любой объект — независимо от его размера — без дополнительных проблем с производительностью или правильностью.
На практике вы часто можете «проскочить» изменяя состояние в React, но мы настоятельно рекомендуем вам этого не делать, чтобы вы могли использовать новые функции React, разработанные с учетом этого подхода. Будущие разработчики и, возможно, даже вы сами в будущем поблагодарят вас!
Резюме
- Обращайтесь со всем состоянием в React как с неизменяемым.
- Когда вы храните объекты в состоянии, их изменение не приведет к перерисовке и изменит состояние в предыдущих «моментальных снимках» рендера.
- Вместо изменения объекта создайте его новую версию и запустите перерисовку, установив состояние в него.
- Вы можете использовать синтаксис разброса объектов
{...obj, something: 'newValue'}для создания копий объектов. - Синтаксис разброса — поверхностный: он копирует только один уровень вглубь.
- Для обновления вложенного объекта необходимо создать копии на всех уровнях сверху от места, которое вы обновляете.
- Чтобы уменьшить повторяющийся код копирования, используйте Immer.
Попробуйте выполнить несколько заданий
Задача 1 из 3:
Исправить некорректные обновления состояния
В этой форме есть несколько ошибок. Нажмите кнопку, которая увеличивает счет несколько раз. Обратите внимание, что это не увеличивается. Затем измените имя, и обратите внимание, что счет внезапно «догоняет» ваши изменения. Наконец, измените фамилию, и обратите внимание, что счет полностью исчез.
Ваша задача — исправить все эти ошибки. По мере их исправления объясните, почему каждая из них происходит.
import { useState } from 'react'; export default function Scoreboard() { const [player, setPlayer] = useState({ firstName: 'Ranjani', lastName: 'Shettar', score: 10, }); function handlePlusClick() { player.score++; } function handleFirstNameChange(e) { setPlayer({ ...player, firstName: e.target.value, }); } function handleLastNameChange(e) { setPlayer({ lastName: e.target.value }); } return ( <> <label> Score: <b>{player.score}</b> {' '} <button onClick={handlePlusClick}> +1 </button> </label> <label> First name: <input value={player.firstName} onChange={handleFirstNameChange} /> </label> <label> Last name: <input value={player.lastName} onChange={handleLastNameChange} /> </label> </> ); }
© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://18.react.dev/learn/updating-objects-in-state