Состояние - память React компонента
2 года назад·1 мин. на чтение
Компонентам часто необходимо изменить то, что отображается на экране в результате взаимодействия. Ввод в форму должен обновить поле ввода, нажатие «Далее» на карусели изображений должно изменить отображаемое изображение, нажатие «купить» должно поместить продукт в корзину. Компоненты должны «запоминать» вещи: текущее входное значение, текущее изображение, корзину. В React такой тип памяти для конкретного компонента называется состоянием.
Содержание туториала по React
Компонентам часто необходимо изменить то, что отображается на экране в результате взаимодействия. Ввод в форму должен обновить поле ввода, нажатие «Далее» на карусели изображений должно изменить отображаемое изображение, нажатие «купить» должно поместить продукт в корзину. Компоненты должны «запоминать» вещи: текущее входное значение, текущее изображение, корзину. В React такой тип памяти для конкретного компонента называется состоянием.
Обработчик события
Теперь нажатие кнопки «Далее» переключает текущее изображение:
Как устроен
Когда вы вызываете
В следующем примере не используется React, но он дает представление о внутренней работе
Также обратите внимание на то, что компонент
Когда обычной переменной недостаточно
Вот компонент, который рендерит изображение скульптуры. Нажатие кнопки «Далее» должно отобразить следующую скульптуру, изменив индекс на 1, затем на 2 и так далее. Однако это не сработает:// App.jsx import { sculptureList } from './data.js'; export default function Gallery() { let index = 0; function handleClick() { index = index + 1; } let sculpture = sculptureList[index]; return ( <> <button onClick={handleClick}>Next</button> <h2> <i>{sculpture.name} </i> by {sculpture.artist} </h2> <h3> ({index + 1} of {sculptureList.length}) </h3> <img src={sculpture.url} alt={sculpture.alt} /> <p>{sculpture.description}</p> </> ); }
// data.js export const sculptureList = [ { name: 'Sculpture 1 Name', artist: 'Sculpture 1 Artist', description: 'Sculpture 1 Description', url: 'https://example.com/sculpture1.jpg', alt: 'Sculpture 1 Description', }, { name: 'Sculpture 2 Name', artist: 'Sculpture 2 Artist', description: 'Sculpture 2 Description', url: 'https://example.com/sculpture2.jpg', alt: 'Sculpture 2 Description', }, ];
handleClick
обновляет локальную переменную index
. Но две вещи препятствуют тому, чтобы это изменение было видимым:
- Локальные переменные не сохраняются между рендерами. Когда React рендерит этот компонент во второй раз, он рендерит его с нуля — он не учитывает никаких изменений в локальных переменных.
- Изменения локальных переменных не вызовут рендеринга. React не понимает, что ему нужно снова визуализировать компонент с новыми данными.
- Данные между рендерами должны сохраняться.
- Нужно заставить React отрендерить компонент с новыми данными (повторный рендеринг).
- Переменную состояния для сохранения данных между рендерами.
- Функцию установки состояния для обновления переменной и запуска React для повторного рендеринга компонента.
Добавление переменной состояния
Чтобы добавить переменную состояния, импортируйтеuseState
из React в начало файла:
Затем замените эту строку:import { useState } from 'react';
на эту:let index = 0;
const [index, setIndex] = useState(0);
index
— это переменная состояния, а setIndex
— функция установки состояния.
Синтаксис [
и ]
называется деструктурированием массива и позволяет считывать значения из массива. Массив, возвращаемый useState
, всегда содержит ровно два элемента.
Вот как они работают вместе в handleClick
:
function handleClick() { setIndex(index + 1); }
// App.jsx import { useState } from 'react'; import { sculptureList } from './data.js'; export default function Gallery() { const [index, setIndex] = useState(0); function handleClick() { setIndex(index + 1); } let sculpture = sculptureList[index]; return ( <> <button onClick={handleClick}>Next</button> <h2> <i>{sculpture.name} </i> by {sculpture.artist} </h2> <h3> ({index + 1} of {sculptureList.length}) </h3> <img src={sculpture.url} alt={sculpture.alt} /> <p>{sculpture.description}</p> </> ); }
Встречайте свой первый хук
В ReactuseState
, как и любая другая функция, начинающаяся с «use
», называется хуком.
Хуки — это специальные функции, которые доступны только во время рендеринга React (о чем мы поговорим подробнее на следующей странице). Они позволяют вам «подключаться» к различным функциям React.
Состояние — это только одна из этих функций, но позже вы познакомитесь с другими хуками.
Хуки — функции, начинающиеся с use
— могут вызываться только на верхнем уровне ваших компонентов или ваших собственных кастомных хуков. Вы не можете вызывать хуки внутри условий, циклов или других вложенных функций. Хуки — это функции, но полезно думать о них как о безусловных декларациях о потребностях вашего компонента. Вы используете (use) функции React в верхней части вашего компонента, подобно тому, как вы «импортируете» модули в верхней части вашего файла.
Как устроен useState
?
Когда вы вызываете useState
, вы сообщаете React, что хотите, чтобы этот компонент что-то запомнил:
В этом случае вы хотите, чтобы React запомнилconst [index, setIndex] = useState(0);
index
.
По соглашению эта пара именуется как const [something, setSomething]
. Вы можете назвать их как угодно, но соглашения облегчают понимание разных проектов.
Единственный аргумент useState
— это начальное значение вашей переменной состояния. В этом примере начальное значение индекса устанавливается равным 0
с помощью useState(0)
.
Каждый раз, когда ваш компонент рендерится, useState
предоставляет вам массив, содержащий два значения:
- Переменная состояния (
index
) со значением, которое вы сохранили. - Функция установки состояния (
setIndex
), которая может обновлять переменную состояния и запускать React для повторного рендеринга компонента.
const [index, setIndex] = useState(0);
- Ваш компонент отображается в первый раз. Поскольку вы передали
0
вuseState
в качестве начального значения дляindex
, он вернет[0, setIndex]
. React помнит, что0
— это последнее значение состояния. - Вы обновляете состояние. Когда пользователь нажимает кнопку, он вызывается
setIndex(index + 1)
.index
равен0
, поэтому этоsetIndex(1)
. Это говорит React запомнить, чтоindex
теперь равен1
, и запускается другой рендеринг. - Второй рендер вашего компонента. React по-прежнему видит
useState(0)
, но поскольку React запомнил, что вы установилиindex
равным1
, вместо этого он возвращает[1, setIndex]
.
Несколько переменных состояния в компоненте
В одном компоненте вы можете иметь столько переменных состояния любого типа, сколько захотите. Этот компонент имеет две переменные состояния, числовойindex
и логическое значение showMore
, которое переключается, когда вы нажимаете "Show details":
Рекомендуется иметь несколько переменных состояния, если их состояние не связано, напримерimport { useState } from 'react'; import { sculptureList } from './data.js'; export default function Gallery() { const [index, setIndex] = useState(0); const [showMore, setShowMore] = useState(false); function handleNextClick() { setIndex(index + 1); } function handleMoreClick() { setShowMore(!showMore); } let sculpture = sculptureList[index]; return ( <> <button onClick={handleNextClick}>Next</button> <h2> <i>{sculpture.name} </i> by {sculpture.artist} </h2> <h3> ({index + 1} of {sculptureList.length}) </h3> <button onClick={handleMoreClick}> {showMore ? 'Hide' : 'Show'} details </button> {showMore && <p>{sculpture.description}</p>} <img src={sculpture.url} alt={sculpture.alt} /> </> ); }
index
и showMore
в этом примере. Но если вы обнаружите, что часто меняете две переменные состояния вместе, может быть лучше объединить их в одну. Например, если у вас есть форма с множеством полей, удобнее иметь одну переменную состояния, которая содержит объект, чем переменную состояния для каждого поля.
В руководстве "Какую структуру состояния выбрать" содержится больше советов по этому вопросу.
Как React узнает, какое состояние вернуть?
Вы могли заметить, что вызовuseState
не получает никакой информации о том, на какую переменную состояния он ссылается. В useState
не передается «идентификатор», так как же он узнает, какую из переменных состояния следует вернуть?
Чтобы обеспечить лаконичный синтаксис, хуки полагаются на стабильный порядок вызовов при каждом рендеринге одного и того же компонента. Это хорошо работает на практике, потому что если вы будете следовать правилу выше («вызывать хуки только на верхнем уровне»), хуки всегда будут вызываться в одном и том же порядке. Кроме того, linter плагин отлавливает большинство ошибок.
Внутри React хранит массив пар состояний для каждого компонента. Он также поддерживает индекс для текущуй пары, который перед рендерингом устанавливается в 0
. Каждый раз, когда вы вызываете useState
, React предоставляет вам следующую пару состояний и увеличивает индекс.
useState
:
Вам не нужно понимать это, чтобы использовать React, но вы можете найти полезную ментальную модель.let componentHooks = []; let currentHookIndex = 0; // Как useState работает внутри React (упрощенно). function useState(initialState) { let pair = componentHooks[currentHookIndex]; if (pair) { // Это не первый рендер, // поэтому пара состояний уже существует. // Верните его и приготовьтесь к следующему хуку. currentHookIndex++; return pair; } // Это первый рендеринг, // поэтому создаем пару состояния и сохраняем ее. pair = [initialState, setState]; function setState(nextState) { // Когда пользователь запрашивает изменение состояния, // помещаем новое значение в пару. pair[0] = nextState; updateDOM(); } // Сохраняем пару для будущих рендеров // и подготовимся к следующему вызову хука. componentHooks[currentHookIndex] = pair; currentHookIndex++; return pair; } function Gallery() { // Каждый вызов useState() будет получать следующую пару. const [index, setIndex] = useState(0); const [showMore, setShowMore] = useState(false); function handleNextClick() { setIndex(index + 1); } function handleMoreClick() { setShowMore(!showMore); } let sculpture = sculptureList[index]; // В этом примере не используется React, //поэтому вернем объект вместо JSX. return { onNextClick: handleNextClick, onMoreClick: handleMoreClick, header: `${sculpture.name} by ${sculpture.artist}`, counter: `${index + 1} of ${sculptureList.length}`, more: `${showMore ? 'Hide' : 'Show'} details`, description: showMore ? sculpture.description : null, imageSrc: sculpture.url, imageAlt: sculpture.alt, }; } function updateDOM() { // Сбросить текущий индекс хука // перед рендерингом компонента. currentHookIndex = 0; let output = Gallery(); // Обновить DOM, чтобы он соответствовал выходным данным. // Это та часть, которую React делает за вас. nextButton.onclick = output.onNextClick; header.textContent = output.header; moreButton.onclick = output.onMoreClick; moreButton.textContent = output.more; image.src = output.imageSrc; image.alt = output.imageAlt; if (output.description !== null) { description.textContent = output.description; description.style.display = ''; } else { description.style.display = 'none'; } } let nextButton = document.getElementById('nextButton'); let header = document.getElementById('header'); let moreButton = document.getElementById('moreButton'); let description = document.getElementById('description'); let image = document.getElementById('image'); let sculptureList = [ { name: 'Sculpture 1 Name', artist: 'Sculpture 1 Artist', description: 'Sculpture 1 Description', url: 'https://example.com/sculpture1.jpg', alt: 'Sculpture 1 Description', }, { name: 'Sculpture 2 Name', artist: 'Sculpture 2 Artist', description: 'Sculpture 2 Description', url: 'https://example.com/sculpture2.jpg', alt: 'Sculpture 2 Description', }, ]; // Сделать интерфейс соответствующим начальному состоянию. updateDOM();
Состояние изолировано и приватно
Состояние является локальным для экземпляра компонента на экране. Другими словами, если вы рендерите один и тот же компонент дважды, каждая копия будет иметь полностью изолированное состояние. Изменение одного из них не повлияет на другой. В этом примере ранее компонентGallery
визуализируется дважды без каких-либо изменений в его логике.
Это то, что отличает состояние от обычных переменных, которые вы можете объявить в верхней части вашего модуля. Состояние не привязано к конкретному вызову функции или месту в коде, но оно «локально» для определенного места на экране. Мы отрендерили два компонентаimport Gallery from './Gallery.js'; export default function Page() { return ( <div className="Page"> <Gallery /> <Gallery /> </div> ); }
<Gallery />
, поэтому их состояние сохраняется отдельно.
Page
ничего не «знает» о состоянии Gallery
и даже о том, есть ли оно у него. В отличие от пропсов, состояние полностью приватно для компонента, объявляющего его. Родительский компонент не может его изменить. Это позволяет добавлять состояние к любому компоненту или удалять его, не затрагивая остальные компоненты.
Что, если вы хотите, чтобы обе галереи синхронизировали свои состояния? Правильный способ сделать это в React — удалить состояние из дочерних компонентов и добавить его к их ближайшему общему родителю. Следующие несколько частей будут посвящены организации состояния одного компонента, но мы вернемся к этой теме в разделе "Обмен данными между компонентами".
Резюме
- Используйте переменную состояния, когда компоненту необходимо «запомнить» некоторую информацию между рендерами.
- Переменные состояния объявляются путем вызова хука
useState
. - Хуки — это специальные функции, которые начинаются с
use
. Они позволяют вам «подключаться» к возможностям React, таким как состояние. - Вызов хуков, включая
useState
, действителен только на верхнем уровне компонента или другого хука. - Хук
useState
возвращает пару значений: текущее состояние и функцию для его обновления. - Вы можете иметь более одной переменной состояния. Внутри React сопоставляет их по порядку.
- Состояние является приватным для компонента. Если вы рендерите его в двух местах, каждая копия получает свое состояние.
Какую структуру состояния React выбрать
2 года назад·12 мин. на чтение
Компонент с хорошо структурированным состоянием приятно модифицировать и легко отлаживать. В этой статье вы найдете советы, которые следует учитывать при структурировании состояния в React компонентах.
Содержание туториала по React
Компонент с хорошо структурированным состоянием приятно модифицировать и легко отлаживать. В этой статье вы найдете советы, которые следует учитывать при структурировании состояния.
Другой случай, когда вы будете группировать данные в объект или массив, — это когда вы не знаете, сколько различных частей состояния вам понадобится. Например, когда у вас есть форма, в которой пользователь может добавлять настраиваемые поля.
Если ваша переменная состояния является объектом, помните, что вы не можете обновить в ней только одно поле без явного копирования других полей. Например, вы не можете использовать
Хотя этот код работает, в нем могут возникнуть «невозможные» состояний. Например, если вы забудете вызвать
Эта форма имеет три переменные состояния:
В настоящее время он сохраняет выбранный элемент как объект в переменной состояния
Обратите внимание, что если вы сначала нажмете «Выбрать» на элементе, а затем отредактируете его, ввод обновится, но метка внизу не отразит изменения. Это потому, что вы дублировали состояние и забыли обновить
В качестве альтернативы вы можете хранить выбранный индекс в состоянии.
Раньше состояние дублировалось так:
Теперь предположим, что вы хотите добавить кнопку для удаления места, которое вы уже посетили. Как бы вы это сделали? Обновление вложенного состояния включает в себя создание копий объектов от той части, которая была изменена. Удаление глубоко вложенного места потребует копирования всей его родительской цепочки мест. Такой код может быть очень многословным.
Если состояние слишком вложенное, чтобы его можно было легко обновить, подумайте о том, чтобы сделать его «плоским». Вот один из способов реструктуризации этих данных. Вместо древовидной структуры, в которой каждое место имеет массив своих дочерних мест, вы можете сделать так, чтобы каждое место содержало массив идентификаторов своих дочерних мест. Затем вы можете сохранить сопоставление каждого идентификатора места с соответствующим местом.
Эта реструктуризация данных может напомнить вам таблицу базы данных:
Теперь, когда состояние «плоское» (также известное как «нормализованное»), обновление вложенных элементов становится проще.
Чтобы удалить место сейчас, вам нужно всего лишь обновить два уровня состояния:
Иногда вы также можете уменьшить вложенность состояний, переместив часть вложенных состояний в дочерние компоненты. Это хорошо работает для временного состояния пользовательского интерфейса, которое не нужно сохранять, например, наведен ли элемент.
Принципы структурирования состояния
Когда вы пишете компонент, который содержит какое-то состояние, вам придется выбирать, сколько переменных состояния использовать и какой должна быть форма их данных. Хотя можно писать правильные программы даже с неоптимальной структурой состояний, есть несколько принципов, которые помогут вам сделать правильный выбор:- Состояние, связанное с группой. Если вы всегда одновременно обновляете две или более переменных состояния, рассмотрите возможность их объединения в одну переменную состояния.
- Избегайте противоречий в состоянии. Когда состояние структурировано таким образом, что несколько частей состояния могут противоречить и «не согласовываться» друг с другом, вы оставляете место для ошибок. Постарайтесь избежать этого.
- Избегайте избыточного состояния. Если вы можете вычислить некоторую информацию из пропсов компонента или его существующих переменных состояния во время рендеринга, вы не должны помещать эту информацию в состояние этого компонента.
- Избегайте дублирования состояния. Когда одни и те же данные дублируются между несколькими переменными состояния или внутри вложенных объектов, их сложно синхронизировать. Уменьшайте дублирование, если возможно.
- Избегайте глубоко вложенных состояний. Глубоко иерархическое состояние не очень удобно обновлять. По возможности предпочтительнее структурировать состояние в плоском виде.
Состояние, связанное с группой
Иногда вы можете быть не уверены, использовать одну или несколько переменных состояния. Должны ли вы это сделать так?Или так?const [x, setX] = useState(0); const [y, setY] = useState(0);
Технически вы можете использовать любой из этих подходов. Но если какие-то две переменные состояния всегда изменяются вместе, было бы неплохо объединить их в одну переменную состояния. Тогда вы не забудете всегда синхронизировать их, как в этом примере, где перемещение курсора обновляет обе координаты красной точки:const [position, setPosition] = useState({ x: 0, y: 0 });
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> ); }
setPosition({ x: 100 })
в приведенном выше примере, потому что у него вообще не будет свойства y
. Вместо этого, если вы хотите установить только x
, вы должны либо выполнить setPosition({ ...position, x: 100})
, либо разделить их на две переменные состояния и выполнить setX(100)
.
Избегайте противоречий в состоянии
Вот форма обратной связи отеля с переменными состоянияisSending
и isSent
:
import { useState } from 'react'; export default function FeedbackForm() { const [text, setText] = useState(''); const [isSending, setIsSending] = useState(false); const [isSent, setIsSent] = useState(false); async function handleSubmit(e) { e.preventDefault(); setIsSending(true); await sendMessage(text); setIsSending(false); setIsSent(true); } if (isSent) { return <h1>Thanks for feedback!</h1>; } return ( <form onSubmit={handleSubmit}> <p>How was your stay at The Prancing Pony?</p> <textarea disabled={isSending} value={text} onChange={(e) => setText(e.target.value)} /> <br /> <button disabled={isSending} type="submit"> Send </button> {isSending && <p>Sending...</p>} </form> ); } // Имитация отправки сообщения. function sendMessage(text) { return new Promise((resolve) => { setTimeout(resolve, 2000); }); }
setIsSent
и setIsSending
вместе, вы можете оказаться в ситуации, когда и isSending
, и isSent
одновременно имеют значение true
. Чем сложнее ваш компонент, тем сложнее будет понять, что произошло.
Поскольку isSending
и isSent
никогда не должны принимать значение true
одновременно, лучше заменить их одной переменной состояния, которая может принимать одно из трех допустимых состояний: «ввод» (typing), «отправка» (sending) и «отправлено» (sent):
Вы также можете объявить некоторые константы для удобства чтения:import { useState } from 'react'; export default function FeedbackForm() { const [text, setText] = useState(''); const [status, setStatus] = useState('typing'); async function handleSubmit(e) { e.preventDefault(); setStatus('sending'); await sendMessage(text); setStatus('sent'); } const isSending = status === 'sending'; const isSent = status === 'sent'; if (isSent) { return <h1>Thanks for feedback!</h1>; } return ( <form onSubmit={handleSubmit}> <p>How was your stay at The Prancing Pony?</p> <textarea disabled={isSending} value={text} onChange={(e) => setText(e.target.value)} /> <br /> <button disabled={isSending} type="submit"> Send </button> {isSending && <p>Sending...</p>} </form> ); } // Имитация отправки сообщения. function sendMessage(text) { return new Promise((resolve) => { setTimeout(resolve, 2000); }); }
Но они не являются переменными состояния, поэтому вам не нужно беспокоиться об их рассинхронизации друг с другом.const isSending = status === 'sending'; const isSent = status === 'sent';
Избегайте избыточного состояния
Если вы можете вычислить некоторую информацию из пропсов компонента или его существующих переменных состояния во время рендеринга, вы не должны помещать эту информацию в состояние этого компонента. Например, рассмотрим такую форму. Он работает, но можете ли вы найти в нем какое-либо избыточное состояние?import { useState } from 'react'; export default function Form() { const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const [fullName, setFullName] = useState(''); function handleFirstNameChange(e) { setFirstName(e.target.value); setFullName(e.target.value + ' ' + lastName); } function handleLastNameChange(e) { setLastName(e.target.value); setFullName(firstName + ' ' + e.target.value); } return ( <> <h2>Let’s check you in</h2> <label> First name: <input value={firstName} onChange={handleFirstNameChange} /> </label> <label> Last name: <input value={lastName} onChange={handleLastNameChange} /> </label> <p> Your ticket will be issued to: <b>{fullName}</b> </p> </> ); }
firstName
, lastName
и fullName
. Однако fullName
является избыточным. Вы всегда можете вычислить fullName
из firstName
и lastName
во время рендеринга, поэтому удалите его из состояния.
Вот как вы можете это сделать:
Здесьimport { useState } from 'react'; export default function Form() { const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const fullName = firstName + ' ' + lastName; function handleFirstNameChange(e) { setFirstName(e.target.value); } function handleLastNameChange(e) { setLastName(e.target.value); } return ( <> <h2>Let’s check you in</h2> <label> First name: <input value={firstName} onChange={handleFirstNameChange} /> </label> <label> Last name: <input value={lastName} onChange={handleLastNameChange} /> </label> <p> Your ticket will be issued to: <b>{fullName}</b> </p> </> ); }
fullName
не является переменной состояния. Вместо этого он вычисляется во время рендеринга:
В результате обработчикам изменений не нужно делать ничего особенного для его обновления. Когда вы вызываетеconst fullName = firstName + ' ' + lastName;
setFirstName
или setLastName
, вы запускаете повторный рендеринг, а затем следующее значение fullName
будет вычислено из свежих данных.
Не отзеркаливайте пропсы в состоянии
Типичным примером избыточного состояния является такой код:Здесь переменная состоянияfunction Message({ messageColor }) { const [color, setColor] = useState(messageColor);
color
инициализируется пропсом messageColor
. Проблема в том, что если родительский компонент позже передаст другое значение messageColor
(например, «красный» вместо «синий»), переменная состояния цвета не будет обновлена. Состояние инициализируется только во время первого рендеринга.
Вот почему «отзеркаливание» некоторых пропсов в переменной состояния может привести к путанице. Вместо этого используйте проп messageColor
непосредственно в коде. Если вы хотите дать ему более короткое имя, используйте константу:
Таким образом, он не будет рассинхронизирован с пропсом, переданным от родительского компонента. «Зеркалирование» пропсов в состояние имеет смысл только в том случае, если вы хотите игнорировать все обновления для определенного пропса. По соглашению начинайте имя пропса сfunction Message({ messageColor }) { const color = messageColor;
initial
или default
, чтобы уточнить, что его новые значения игнорируются:
function Message({ initialColor }) { // Переменная состояния `color` содержит начальное значение `initialColor`. // Последующие изменения пропса `initialColor` игнорируются. const [color, setColor] = useState(initialColor);
Избегайте дублирования состояния
Этот компонент меню позволяет выбрать один перекус из нескольких:import { useState } from 'react'; const initialItems = [ { title: 'pretzels', id: 0 }, { title: 'crispy seaweed', id: 1 }, { title: 'granola bar', id: 2 }, ]; export default function Menu() { const [items, setItems] = useState(initialItems); const [selectedItem, setSelectedItem] = useState(items[0]); return ( <> <h2>What's your travel snack?</h2> <ul> {items.map((item) => ( <li key={item.id}> {item.title}{' '} <button onClick={() => { setSelectedItem(item); }} > Choose </button> </li> ))} </ul> <p>You picked {selectedItem.title}.</p> </> ); }
selectedItem
. Однако это не очень хорошо: содержимое selectedItem
— это тот же объект, что и один из элементов в списке элементов. Это означает, что информация о самом предмете дублируется в двух местах.
Почему это проблема? Давайте сделаем каждый элемент редактируемым:
import { useState } from 'react'; const initialItems = [ { title: 'pretzels', id: 0 }, { title: 'crispy seaweed', id: 1 }, { title: 'granola bar', id: 2 }, ]; export default function Menu() { const [items, setItems] = useState(initialItems); const [selectedItem, setSelectedItem] = useState(items[0]); function handleItemChange(id, e) { setItems( items.map((item) => { if (item.id === id) { return { ...item, title: e.target.value, }; } else { return item; } }) ); } return ( <> <h2>What's your travel snack?</h2> <ul> {items.map((item, index) => ( <li key={item.id}> <input value={item.title} onChange={(e) => { handleItemChange(item.id, e); }} />{' '} <button onClick={() => { setSelectedItem(item); }} > Choose </button> </li> ))} </ul> <p>You picked {selectedItem.title}.</p> </> ); }
selectedItem
.
Хотя вы также можете обновить selectedItem
, более простое решение — удалить дублирование. В этом примере вместо объекта selectedItem
(который создает дублирование объектов внутри элементов) вы сохраняете selectedId
в состоянии, а затем получаете selectedItem
путем поиска в массиве элементов элемента с этим идентификатором:
import { useState } from 'react'; const initialItems = [ { title: 'pretzels', id: 0 }, { title: 'crispy seaweed', id: 1 }, { title: 'granola bar', id: 2 }, ]; export default function Menu() { const [items, setItems] = useState(initialItems); const [selectedId, setSelectedId] = useState(0); const selectedItem = items.find((item) => item.id === selectedId); function handleItemChange(id, e) { setItems( items.map((item) => { if (item.id === id) { return { ...item, title: e.target.value, }; } else { return item; } }) ); } return ( <> <h2>What's your travel snack?</h2> <ul> {items.map((item, index) => ( <li key={item.id}> <input value={item.title} onChange={(e) => { handleItemChange(item.id, e); }} />{' '} <button onClick={() => { setSelectedId(item.id); }} > Choose </button> </li> ))} </ul> <p>You picked {selectedItem.title}.</p> </> ); }
items = [{ id: 0, title: 'pretzels'}, ...]
selectedItem = {id: 0, title: 'pretzels'}
items = [{ id: 0, title: 'pretzels'}, ...]
selectedId = 0
setItems
вызывает повторный рендеринг, а items.find(...)
найдет элемент с обновленным заголовком. Вам не нужно было удерживать выбранный элемент в состоянии, потому что важен только выбранный идентификатор. Остальное можно рассчитать во время рендера.
Избегайте глубоко вложенных состояний
Представьте план путешествия, состоящий из планет, континентов и стран. У вас может возникнуть соблазн структурировать его состояние с помощью вложенных объектов и массивов, как в этом примере:// App.jsx import { useState } from 'react'; import { initialTravelPlan } from './places.js'; function PlaceTree({ place }) { const childPlaces = place.childPlaces; return ( <li> {place.title} {childPlaces.length > 0 && ( <ol> {childPlaces.map((place) => ( <PlaceTree key={place.id} place={place} /> ))} </ol> )} </li> ); } export default function TravelPlan() { const [plan, setPlan] = useState(initialTravelPlan); const planets = plan.childPlaces; return ( <> <h2>Places to visit</h2> <ol> {planets.map((place) => ( <PlaceTree key={place.id} place={place} /> ))} </ol> </> ); }
// places.js export const initialTravelPlan = { id: 0, title: '(Root)', childPlaces: [ { id: 1, title: 'Earth', childPlaces: [ { id: 2, title: 'Africa', childPlaces: [ { id: 3, title: 'Botswana', childPlaces: [], }, { id: 4, title: 'Egypt', childPlaces: [], }, ], }, { id: 5, title: 'Asia', childPlaces: [ { id: 6, title: 'China', childPlaces: [], }, { id: 7, title: 'Hong Kong', childPlaces: [], }, ], }, ], }, { id: 8, title: 'Mars', childPlaces: [ { id: 9, title: 'Corn Town', childPlaces: [], }, { id: 10, title: 'Green Hill', childPlaces: [], }, ], }, ], };
// App.jsx import { useState } from 'react'; import { initialTravelPlan } from './places.js'; function PlaceTree({ id, placesById }) { const place = placesById[id]; const childIds = place.childIds; return ( <li> {place.title} {childIds.length > 0 && ( <ol> {childIds.map((childId) => ( <PlaceTree key={childId} id={childId} placesById={placesById} /> ))} </ol> )} </li> ); } export default function TravelPlan() { const [plan, setPlan] = useState(initialTravelPlan); const root = plan[0]; const planetIds = root.childIds; return ( <> <h2>Places to visit</h2> <ol> {planetIds.map((id) => ( <PlaceTree key={id} id={id} placesById={plan} /> ))} </ol> </> ); }
// places.js export const initialTravelPlan = { 0: { id: 0, title: '(Root)', childIds: [1, 8], }, 1: { id: 1, title: 'Earth', childIds: [2, 5], }, 2: { id: 2, title: 'Africa', childIds: [3, 4], }, 3: { id: 3, title: 'Botswana', childIds: [], }, 4: { id: 4, title: 'Egypt', childIds: [], }, 5: { id: 5, title: 'Asia', childIds: [6, 7], }, 6: { id: 6, title: 'China', childIds: [], }, 7: { id: 7, title: 'Hong Kong', childIds: [], }, 8: { id: 8, title: 'Mars', childIds: [9, 10], }, 9: { id: 9, title: 'Corn Town', childIds: [], }, 10: { id: 10, title: 'Green Hill', childIds: [], }, };
- Обновленная версия родительского места должна исключить удаленный идентификатор из массива
childIds
. - Обновленная версия корневого объекта должна включать обновленную версию родительского места.
Вы можете сколько угодно вкладывать состояния, но если сделать их «плоскими», это может решить множество проблем. Это упрощает обновление состояния и помогает избежать дублирования в разных частях вложенного объекта.// App.jsx import { useState } from 'react'; import { initialTravelPlan } from './places.js'; export default function TravelPlan() { const [plan, setPlan] = useState(initialTravelPlan); function handleComplete(parentId, childId) { const parent = plan[parentId]; // Создаем новую версию родительского места, // которая не включает этот дочерний ID. const nextParent = { ...parent, childIds: parent.childIds.filter((id) => id !== childId), }; // Обновляем состояние корневого объекта... setPlan({ ...plan, // ...чтобы он содержал обновленный объект родителя. [parentId]: nextParent, }); } const root = plan[0]; const planetIds = root.childIds; return ( <> <h2>Places to visit</h2> <ol> {planetIds.map((id) => ( <PlaceTree key={id} id={id} parentId={0} placesById={plan} onComplete={handleComplete} /> ))} </ol> </> ); } function PlaceTree({ id, parentId, placesById, onComplete }) { const place = placesById[id]; const childIds = place.childIds; return ( <li> {place.title} <button onClick={() => { onComplete(parentId, id); }} > Complete </button> {childIds.length > 0 && ( <ol> {childIds.map((childId) => ( <PlaceTree key={childId} id={childId} parentId={id} placesById={placesById} onComplete={onComplete} /> ))} </ol> )} </li> ); }
Улучшение использования памяти
В идеале вы также должны удалить удаленные элементы (и их дочерние элементы) из объекта-таблицы, чтобы оптимизировать использование памяти. Реализация ниже содержит эту логику. Он также использует Immer, чтобы сделать логику обновления более лаконичной.import { useImmer } from 'use-immer'; import { initialTravelPlan } from './places.js'; export default function TravelPlan() { const [plan, updatePlan] = useImmer(initialTravelPlan); function handleComplete(parentId, childId) { updatePlan((draft) => { // Удлаим дочерние ID из родительского `place`. const parent = draft[parentId]; parent.childIds = parent.childIds.filter((id) => id !== childId); // Удалим это место и все его поддерево. deleteAllChildren(childId); function deleteAllChildren(id) { const place = draft[id]; place.childIds.forEach(deleteAllChildren); delete draft[id]; } }); } const root = plan[0]; const planetIds = root.childIds; return ( <> <h2>Places to visit</h2> <ol> {planetIds.map((id) => ( <PlaceTree key={id} id={id} parentId={0} placesById={plan} onComplete={handleComplete} /> ))} </ol> </> ); } function PlaceTree({ id, parentId, placesById, onComplete }) { const place = placesById[id]; const childIds = place.childIds; return ( <li> {place.title} <button onClick={() => { onComplete(parentId, id); }} > Complete </button> {childIds.length > 0 && ( <ol> {childIds.map((childId) => ( <PlaceTree key={childId} id={childId} parentId={id} placesById={placesById} onComplete={onComplete} /> ))} </ol> )} </li> ); }
Резюме
- Если две переменные состояния всегда обновляются вместе, рассмотрите возможность их объединения в одну.
- Тщательно выбирайте переменные состояния, чтобы избежать создания «невозможных» состояний.
- Структурируйте свое состояние таким образом, чтобы уменьшить вероятность того, что вы совершите ошибку при его обновлении.
- Избегайте избыточного и дублирующего состояния, чтобы вам не нужно было синхронизировать его.
- Не помещайте пропсы в состояние, если вы специально не хотите предотвратить обновления.
- Для шаблонов пользовательского интерфейса, таких как выбор, сохраняйте в состоянии не сам объект, а его идентификатор или индекс.
- Если обновление глубоко вложенного состояния затруднено, попробуйте сделать его плоским.