v19.2Latest

Выбор структуры состояния

Хорошо структурированное состояние может сделать разницу между компонентом, который приятно изменять и отлаживать, и тем, который является постоянным источником ошибок. Вот несколько советов, которые следует учитывать при структурировании состояния.

Вы изучите
  • Когда использовать одну или несколько переменных состояния
  • Чего следует избегать при организации состояния
  • Как исправить распространённые проблемы со структурой состояния

Принципы структурирования состояния

Когда вы пишете компонент, который содержит некоторое состояние, вам придётся делать выбор о том, сколько переменных состояния использовать и какой должна быть форма их данных. Хотя можно написать корректные программы даже с неоптимальной структурой состояния, есть несколько принципов, которые могут направить вас к лучшему выбору:

  1. Группируйте связанное состояние.Если вы всегда обновляете две или более переменных состояния одновременно, рассмотрите возможность объединения их в одну переменную состояния.
  2. Избегайте противоречий в состоянии.Когда состояние структурировано таким образом, что несколько частей состояния могут противоречить и «не соглашаться» друг с другом, вы оставляете место для ошибок. Старайтесь этого избегать.
  3. Избегайте избыточного состояния.Если вы можете вычислить некоторую информацию из пропсов компонента или его существующих переменных состояния во время рендеринга, вам не следует помещать эту информацию в состояние этого компонента.
  4. Избегайте дублирования в состоянии.Когда одни и те же данные дублируются между несколькими переменными состояния или внутри вложенных объектов, их сложно поддерживать синхронизированными. Уменьшайте дублирование, когда это возможно.
  5. Избегайте глубоко вложенного состояния.Глубоко иерархическое состояние не очень удобно обновлять. По возможности предпочитайте структурировать состояние плоским способом.

Цель этих принципов —сделать состояние лёгким для обновления без внесения ошибок. Удаление избыточных и дублирующихся данных из состояния помогает гарантировать, что все его части остаются синхронизированными. Это похоже на то, как инженер баз данных может захотеть«нормализовать» структуру базы данных, чтобы уменьшить вероятность ошибок. Перефразируя Альберта Эйнштейна,«Сделайте ваше состояние настолько простым, насколько это возможно, но не проще».

Теперь давайте посмотрим, как эти принципы применяются на практике.

Иногда вы можете быть не уверены, использовать одну или несколько переменных состояния.

Следует ли делать так?

Или так?

Технически вы можете использовать любой из этих подходов. Ноесли какие-то две переменные состояния всегда меняются вместе, возможно, стоит объединить их в одну переменную состояния.Тогда вы не забудете всегда держать их синхронизированными, как в этом примере, где перемещение курсора обновляет обе координаты красной точки:

Другой случай, когда вы будете группировать данные в объект или массив, — это когда вы не знаете, сколько частей состояния вам понадобится. Например, это полезно, когда у вас есть форма, в которой пользователь может добавлять пользовательские поля.

Подводный камень

Если ваша переменная состояния является объектом, помните, чтовы не можете обновить только одно поле в нёмбез явного копирования других полей. Например, вы не можете сделатьsetPosition({ x: 100 })в приведённом выше примере, потому что тогда у него вообще не будет свойстваy! Вместо этого, если вы хотите установить толькоx, вы должны либо сделатьsetPosition({ ...position, x: 100 }), либо разделить их на две переменные состояния и сделатьsetX(100).

Избегайте противоречий в состоянии

Вот форма обратной связи отеля с переменными состоянияisSending и isSent:

Хотя этот код работает, он оставляет возможность для «невозможных» состояний. Например, если вы забудете вызватьsetIsSent и setIsSendingвместе, вы можете оказаться в ситуации, когда иisSending, иisSentодновременно имеют значениеtrue. Чем сложнее ваш компонент, тем труднее понять, что произошло.

ПосколькуisSending и isSentникогда не должны бытьtrueодновременно, лучше заменить их одной переменной состоянияstatus, которая может принимать одно изтрёхдопустимых значений:'typing'(начальное),'sending' и 'sent':

Вы всё ещё можете объявить несколько констант для удобочитаемости:

Но это не переменные состояния, поэтому вам не нужно беспокоиться о том, что они рассинхронизируются друг с другом.

Избегайте избыточного состояния

Если вы можете вычислить некоторую информацию из пропсов компонента или его существующих переменных состояния во время рендеринга, вамне следуетпомещать эту информацию в состояние этого компонента.

Например, рассмотрим эту форму. Она работает, но можете ли вы найти в ней избыточное состояние?

Эта форма имеет три переменные состояния:firstName,lastName и fullName. ОднакоfullNameявляется избыточной.Вы всегда можете вычислитьfullNameизfirstName и lastNameво время рендеринга, поэтому удалите её из состояния.

Вот как это можно сделать:

ЗдесьfullNameнепеременная состояния. Вместо этого она вычисляется во время рендеринга:

В результате обработчикам изменений не нужно делать ничего особенного для её обновления. Когда вы вызываетеsetFirstNameилиsetLastName, вы запускаете повторный рендеринг, и тогда следующее значениеfullNameбудет вычислено из свежих данных.

Deep Dive
Не зеркалируйте пропсы в состоянии

Избегайте дублирования в состоянии

Этот компонент списка меню позволяет выбрать одну закуску для путешествия из нескольких:

В настоящее время он хранит выбранный элемент как объект в переменной состоянияselectedItem. Однако это не очень хорошо:содержимоеselectedItem— это тот же самый объект, что и один из элементов внутри спискаitems.Это означает, что информация о самом элементе дублируется в двух местах.

Почему это проблема? Давайте сделаем каждый элемент редактируемым:

Обратите внимание, что если вы сначала нажмёте «Choose» на элементе, азатемотредактируете его,поле ввода обновится, но метка внизу не отразит изменения.Это происходит потому, что у вас дублируется состояние, и вы забыли обновитьselectedItem.

Хотя вы могли бы обновить иselectedItem, более простое решение — устранить дублирование. В этом примере вместо объектаselectedItem(который создаёт дублирование с объектами внутриitems) вы хранитеselectedIdв состоянии, азатемполучаетеselectedItem, выполняя поиск в массивеitemsэлемента с этим ID:

Состояние раньше дублировалось так:

  • items = [{ id: 0, title: 'pretzels'}, ...]
  • selectedItem = {id: 0, title: 'pretzels'}

Но после изменения оно стало таким:

  • items = [{ id: 0, title: 'pretzels'}, ...]
  • selectedId = 0

Дублирование исчезло, и вы храните только необходимое состояние!

Теперь, если вы отредактируетевыбранныйэлемент, сообщение ниже обновится мгновенно. Это происходит потому, чтоsetItemsвызывает повторный рендер, иitems.find(...)найдёт элемент с обновлённым заголовком. Вам не нужно было хранитьвыбранный элементв состоянии, потому что важным был тольковыбранный ID. Остальное можно было вычислить во время рендера.

Избегайте глубоко вложенного состояния

Представьте план путешествия, состоящий из планет, континентов и стран. Возможно, вам захочется структурировать его состояние с помощью вложенных объектов и массивов, как в этом примере:

Теперь предположим, что вы хотите добавить кнопку для удаления места, которое уже посетили. Как бы вы это сделали?Обновление вложенного состоянияподразумевает создание копий объектов на всём пути от изменённой части. Удаление глубоко вложенного места потребует копирования всей цепочки его родительских мест. Такой код может быть очень многословным.

Если состояние слишком вложено для лёгкого обновления, подумайте о том, чтобы сделать его «плоским».Вот один из способов реструктуризации этих данных. Вместо древовидной структуры, где каждыйplaceсодержит массивсвоих дочерних мест, можно сделать так, чтобы каждое место хранило массивID своих дочерних мест. Затем сохраните отображение каждого ID места в соответствующее место.

Эта реструктуризация данных может напомнить вам таблицу базы данных:

Теперь, когда состояние стало «плоским» (также называемым «нормализованным»), обновление вложенных элементов становится проще.

Чтобы удалить место сейчас, вам нужно обновить только два уровня состояния:

  • Обновлённая версия егородительскогоместа должна исключать удалённый ID из своего массиваchildIds.
  • Обновлённая версия корневого «табличного» объекта должна включать обновлённую версию родительского места.

Вот пример того, как это можно сделать:

Вы можете вкладывать состояние на любую глубину, но его «уплощение» может решить множество проблем. Это упрощает обновление состояния и помогает избежать дублирования в разных частях вложенного объекта.

Иногда вы также можете уменьшить вложенность состояния, переместив часть вложенного состояния в дочерние компоненты. Это хорошо работает для временного состояния интерфейса, которое не нужно сохранять, например, для отслеживания наведения курсора на элемент.

Итоги

  • Если две переменные состояния всегда обновляются вместе, рассмотрите возможность их объединения в одну.
  • Тщательно выбирайте переменные состояния, чтобы избежать создания «невозможных» состояний.
  • Структурируйте состояние так, чтобы снизить вероятность ошибок при его обновлении.
  • Избегайте избыточного и дублирующего состояния, чтобы не приходилось синхронизировать его.
  • Не помещайте пропсывсостояние, если только вы не хотите явно предотвратить их обновление.
  • Для паттернов интерфейса, таких как выбор, храните в состоянии ID или индекс, а не сам объект.
  • Если обновление глубоко вложенного состояния сложно, попробуйте уплостить его.

Try out some challenges

Challenge 1 of 4:Fix a component that’s not updating #

This Clock component receives two props: color and time. When you select a different color in the select box, the Clock component receives a different color prop from its parent component. However, for some reason, the displayed color doesn’t update. Why? Fix the problem.