Обновление объектов в состоянии¶
Состояние может хранить любые значения JavaScript, включая объекты. Но вы не должны изменять объекты, которые хранятся в состоянии React, напрямую. Вместо этого, когда вы хотите обновить объект, вам нужно создать новый (или сделать копию существующего), а затем настроить состояние на использование этой копии.
Вы узнаете
- Как правильно обновить объект в состоянии React
- Как обновить вложенный объект без его мутирования
- Что такое неизменяемость и как ее не нарушить
- Как сделать копирование объектов менее повторяющимся с помощью Immer
Что такое мутация?¶
В состоянии можно хранить любые значения JavaScript.
1 |
|
До сих пор вы работали с числами, строками и булевыми числами. Эти типы значений JavaScript являются "неизменяемыми", то есть неизменяемыми или "только для чтения". Чтобы заменить значение, можно вызвать повторный рендеринг:
1 |
|
Состояние x
изменилось с 0
на 5
, но само число 0
не изменилось. В JavaScript невозможно внести какие-либо изменения во встроенные примитивные значения, такие как числа, строки и булевы.
Теперь рассмотрим объект в состоянии:
1 |
|
Технически, можно изменить содержимое самого объекта. Это называется мутацией:.
1 |
|
Однако, хотя объекты в React state технически являются изменяемыми, вы должны относиться к ним как к неизменяемым, как к числам, булевым числам и строкам. Вместо того чтобы изменять их, вы всегда должны заменять их.
Рассматривайте состояние как доступное только для чтения¶
Другими словами, вы должны относиться к любому объекту JavaScript, который вы помещаете в состояние, как к объекту только для чтения.
В этом примере в состоянии находится объект, представляющий текущее положение указателя. Красная точка должна перемещаться при касании или перемещении курсора по области предварительного просмотра. Но точка остается в исходном положении:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
Проблема заключается в этом фрагменте кода.
1 2 3 4 |
|
Этот код изменяет объект, назначенный на position
из предыдущего рендера. Но без использования функции установки состояния React не знает, что объект изменился. Поэтому React ничего не делает в ответ. Это все равно что пытаться изменить заказ после того, как вы уже поели. Хотя мутирование состояния может работать в некоторых случаях, мы не рекомендуем этого делать. Вы должны рассматривать значение состояния, к которому вы имеете доступ во время рендеринга, как доступное только для чтения.
Чтобы действительно вызвать повторный рендеринг в этом случае, создайте новый объект и передайте его в функцию установки состояния:.
1 2 3 4 5 6 |
|
С помощью setPosition
вы говорите React:
- Замените
position
на этот новый объект. - И снова отобразите этот компонент
Обратите внимание, что красная точка теперь следует за вашим указателем, когда вы касаетесь или наводите курсор на область предварительного просмотра:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
Локальная мутация — это нормально
Код, подобный этому, является проблемой, потому что он изменяет существующий объект в состоянии:
1 2 |
|
Но такой код абсолютно нормален, потому что вы мутируете свежий объект, который вы только что создали:
1 2 3 4 |
|
На самом деле, это совершенно равносильно тому, чтобы написать это:
1 2 3 4 |
|
Мутация является проблемой только тогда, когда вы изменяете существующие объекты, которые уже находятся в состоянии. Мутация только что созданного объекта - это нормально, потому что на него пока не ссылается никакой другой код. Изменение объекта не окажет случайного влияния на что-то, что от него зависит. Это называется "локальной мутацией". Вы даже можете делать локальную мутацию во время рендеринга Очень удобно и совершенно нормально!
Копирование объектов с синтаксисом ...
¶
В предыдущем примере объект position
всегда создается свежим из текущей позиции курсора. Но часто возникает необходимость включить существующие данные как часть нового создаваемого объекта. Например, вы можете захотеть обновить только одно поле в форме, но сохранить прежние значения для всех остальных полей.
Такие поля ввода не работают, потому что обработчики onChange
изменяют состояние:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
|
Например, эта строка мутирует состояние из прошлого рендеринга:
1 |
|
Надежный способ добиться нужного вам поведения — создать новый объект и передать его в setPerson
. Но здесь вы хотите также копировать в него существующие данные, поскольку изменилось только одно из полей:
1 2 3 4 5 |
|
Вы можете использовать спреад-синтаксис ...
распространение объекта, чтобы не копировать каждое свойство отдельно.
1 2 3 4 |
|
Теперь форма работает!
Обратите внимание, что вы не объявили отдельную переменную состояния для каждого поля ввода. Для больших форм очень удобно хранить все данные, сгруппированные в одном объекте — при условии, что вы правильно их обновляете!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
|
Обратите внимание, что синтаксис распространения ...
является "неглубоким" - он копирует объекты только на один уровень вглубь. Это делает его быстрым, но это также означает, что если вы хотите обновить вложенное свойство, вам придется использовать его несколько раз.
Использование одного обработчика событий для нескольких полей
Вы также можете использовать скобки [
и ]
внутри определения объекта, чтобы указать свойство с динамическим именем. Вот тот же пример, но с одним обработчиком событий вместо трех разных:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
|
Здесь e.target.name
относится к свойству name
, заданному DOM-элементу <input>
.
Обновление вложенного объекта¶
Рассмотрим структуру вложенного объекта следующим образом:
1 2 3 4 5 6 7 8 |
|
Если вы хотите обновить person.artwork.city
, то понятно, как это сделать с помощью мутации:
1 |
|
Но в React состояние рассматривается как неизменяемое! Чтобы изменить city
, вам сначала нужно создать новый объект artwork
(предварительно заполненный данными из предыдущего объекта), а затем создать новый объект person
, который указывает на новый artwork
:
1 2 3 4 5 6 |
|
Или записанный как вызов одной функции:
1 2 3 4 5 6 7 8 |
|
Это немного многословно, но для многих случаев подходит:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
|
Объекты на самом деле не являются вложенными
Объект, подобный этому, появляется "вложенным" в код:
1 2 3 4 5 6 7 8 |
|
Однако "вложенность" - это неточный способ представления о том, как ведут себя объекты. Когда код выполняется, не существует такого понятия, как "вложенный" объект. На самом деле вы рассматриваете два разных объекта:
1 2 3 4 5 6 7 8 9 10 |
|
Объект obj1
не находится "внутри" obj2
. Например, obj3
может "указывать" и на obj1
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Если бы вы изменили obj3.artwork.city
, это повлияло бы и на obj2.artwork.city
, и на obj1.city
. Это происходит потому, что obj3.artwork
, obj2.artwork
и obj1
являются одним и тем же объектом. Это трудно заметить, когда вы думаете об объектах как о "вложенных". Вместо этого они представляют собой отдельные объекты, "указывающие" друг на друга с помощью свойств.
Напишите лаконичную логику обновления с помощью Immer¶
Если ваше состояние глубоко вложенное, вы можете рассмотреть возможность сглаживания. Но если вы не хотите менять структуру состояния, вы можете предпочесть быстрый путь к вложенным спредам. Immer - это популярная библиотека, которая позволяет вам писать, используя удобный, но мутирующий синтаксис, и заботится о создании копий за вас. С Immer написанный вами код выглядит так, как будто вы "нарушаете правила" и мутируете объект:
1 2 3 |
|
Но в отличие от обычной мутации, она не переписывает прошлое состояние!
Как работает Immer?
"Черновик", предоставляемый Immer, является особым типом объекта, называемым Proxy, который "записывает" то, что вы с ним делаете. Именно поэтому вы можете свободно мутировать его сколько угодно! Под капотом Immer определяет, какие части черновика
были изменены, и создает совершенно новый объект, содержащий ваши правки.
Чтобы попробовать Immer:
- Запустите
npm install use-immer
, чтобы добавить Immer в качестве зависимости. - Затем замените
import { useState } из 'react'
наimport { useImmer } из 'use-immer'
.
Вот вышеприведенный пример, преобразованный в Immer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
|
Обратите внимание, насколько более лаконичными стали обработчики событий. Вы можете смешивать и сочетать useState
и useImmer
в одном компоненте сколько угодно. Immer — отличный способ сохранить обработчики обновлений лаконичными, особенно если в вашем состоянии есть вложенность, и копирование объектов приводит к повторяющемуся коду.
Почему мутирование состояния не рекомендуется в React?
Есть несколько причин:
- Отладка: Если вы используете
console.log
и не мутируете состояние, ваши прошлые логи не будут забиты недавними изменениями состояния. Таким образом, вы можете четко видеть, как изменялось состояние между рендерами. - Оптимизация: Обычные стратегии оптимизации React полагаются на пропуск работы, если предыдущие пропсы или состояние совпадают с последующими. Если вы никогда не изменяете состояние, то проверить, были ли изменения, можно очень быстро. Если
prevObj === obj
, вы можете быть уверены, что внутри него ничего не могло измениться. - Новые возможности: Новые возможности React, которые мы создаем, зависят от того, что состояние рассматривается как снимок. Если вы мутируете прошлые версии состояния, это может помешать вам использовать новые возможности.
- Изменения требований: Некоторые возможности приложения, такие как реализация Undo/Redo, показ истории изменений или предоставление пользователю возможности вернуть форму к прежним значениям, проще сделать, если ничего не мутировать. Это происходит потому, что вы можете хранить в памяти прошлые копии состояния и использовать их повторно, когда это необходимо. Если вы начнете с мутативного подхода, такие функции, как эта, будет трудно добавить позже.
- Простая реализация: Поскольку React не полагается на мутацию, ему не нужно делать ничего особенного с вашими объектами. Ему не нужно перехватывать их свойства, всегда оборачивать их в прокси или выполнять другую работу при инициализации, как это делают многие "реактивные" решения. Именно поэтому React позволяет вам поместить любой объект в состояние - независимо от его размера - без дополнительных проблем с производительностью и корректностью.
На практике вы часто можете "уйти" от мутирования состояния в React, но мы настоятельно рекомендуем вам не делать этого, чтобы вы могли использовать новые возможности React, разработанные с учетом этого подхода. Будущие разработчики и, возможно, даже ваше будущее "я" скажут вам спасибо!
Резюме
- Рассматривайте все состояния в React как неизменяемые.
- Когда вы храните объекты в состоянии, их изменение не вызовет рендеринга и изменит состояние в предыдущих "снимках" рендеринга.
- Вместо того чтобы мутировать объект, создайте новую его версию и вызовите повторный рендеринг, установив для нее состояние.
- Вы можете использовать синтаксис распространения объектов
{...obj, something: 'newValue'}
для создания копий объектов. - Синтаксис распространения является неглубоким: он копирует только один уровень в глубину.
- Чтобы обновить вложенный объект, вам нужно создать копии по всему пути вверх от того места, которое вы обновляете.
- Чтобы уменьшить количество повторяющегося кода копирования, используйте Immer.
Задачи¶
1. Исправление некорректных обновлений состояния¶
В этой форме есть несколько ошибок. Несколько раз нажмите на кнопку, увеличивающую оценку. Заметьте, что он не увеличивается. Затем отредактируйте имя и фамилию и заметите, что оценка внезапно "подхватила" ваши изменения. Наконец, отредактируйте фамилию, и заметите, что оценка полностью исчезла.
Ваша задача — исправить все эти ошибки. Исправляя их, объясните, почему происходит каждая из них.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
|
Показать решение
Вот версия, в которой исправлены обе ошибки:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
|
Проблема с handlePlusClick
заключалась в том, что он мутировал объект player
. В результате React не знал, что есть причина для повторного рендеринга, и не обновлял счет на экране. Вот почему, когда вы редактировали первое имя, состояние обновлялось, вызывая повторный рендеринг, который также обновлял счет на экране.
Проблема с handleLastNameChange
заключалась в том, что он не копировал существующие поля ...player
в новый объект. Вот почему счет терялся после редактирования фамилии.
2. Найти и исправить мутацию¶
Имеется перетаскиваемый ящик на статичном фоне. Вы можете изменить цвет поля с помощью кнопки select.
Но есть ошибка. Если сначала переместить ящик, а затем изменить его цвет, фон (который не должен двигаться!) "перепрыгнет" на позицию ящика. Но этого не должно произойти: параметр position
у Background
установлен в initialPosition
, что равно { x: 0, y: 0 }
. Почему фон перемещается после изменения цвета?
Найдите ошибку и исправьте ее.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Показать подсказку
Если что-то неожиданно меняется, значит, произошла мутация. Найдите мутацию в App.js
и исправьте ее.
Показать решение
Проблема была в мутации внутри handleMove
. Она мутировала shape.position
, но это тот же объект, на который указывает initialPosition
. Поэтому и форма, и фон перемещаются. (Это мутация, поэтому изменение не отражается на экране до тех пор, пока не произойдет несвязанное обновление - изменение цвета - не вызовет повторный рендеринг).
Исправление заключается в удалении мутации из handleMove
и использовании синтаксиса распространения для копирования формы. Обратите внимание, что +=
- это мутация, поэтому вам нужно переписать ее, чтобы использовать обычную операцию +
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
3. Обновление объекта с помощью Immer¶
Это тот же пример с ошибкой, что и в предыдущей задаче. На этот раз исправьте мутацию, используя Immer. Для вашего удобства функция useImmer
уже импортирована, поэтому вам нужно изменить переменную состояния shape
, чтобы использовать ее.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Показать решение
Это решение переписано с помощью Immer. Обратите внимание, что обработчики событий написаны мутирующим образом, но ошибка не возникает. Это потому, что Immer никогда не мутирует существующие объекты.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Источник — https://react.dev/learn/updating-objects-in-state