Управление состоянием
По мере роста приложения становится полезно более целенаправленно подходить к организации состояния и способам передачи данных между компонентами. Избыточное или дублированное состояние — распространённый источник ошибок. В этой главе вы узнаете, как структурировать состояние, как поддерживать логику обновления состояния и как обмениваться состоянием между удалёнными компонентами.
В этой главе
- Как рассматривать изменения пользовательского интерфейса как изменения состояния
- Как правильно структурировать состояние
- Как «поднимать состояние вверх», чтобы обмениваться им между компонентами
- Как управлять тем, сохраняется ли состояние или сбрасывается
- Как консолидировать сложную логику состояния в функции
- Как передавать информацию без «буксировки свойств»
- Как масштабировать управление состоянием по мере роста приложения
Реагирование на ввод с помощью состояния
В React вы не будете напрямую изменять пользовательский интерфейс из кода. Например, вы не будете писать команды типа «отключить кнопку», «включить кнопку», «отобразить сообщение об успехе» и т. д. Вместо этого вы опишете желаемый пользовательский интерфейс для различных визуальных состояний вашего компонента («начальное состояние», «состояние ввода», «состояние успеха») и затем вызовете изменения состояния в ответ на пользовательский ввод. Это похоже на то, как дизайнеры думают об интерфейсе пользователя.
Вот форма для викторины, созданная с использованием React. Обратите внимание, как она использует переменную состояния status для определения того, следует ли включать или отключать кнопку отправки и следует ли отобразить сообщение об успехе вместо неё.
import { useState } from 'react'; export default function Form() { const [answer, setAnswer] = useState(''); const [error, setError] = useState(null); const [status, setStatus] = useState('typing'); if (status === 'success') { return <h1>That's right!</h1> } async function handleSubmit(e) { e.preventDefault(); setStatus('submitting'); try { await submitForm(answer); setStatus('success'); } catch (err) { setStatus('typing'); setError(err); } } function handleTextareaChange(e) { setAnswer(e.target.value); } return ( <> <h2>City quiz</h2> <p> In which city is there a billboard that turns air into drinkable water? </p> <form onSubmit={handleSubmit}> <textarea value={answer} onChange={handleTextareaChange} disabled={status === 'submitting'} /> <br /> <button disabled={ answer.length === 0 || status === 'submitting' }> Submit </button> {error !== null && <p className="Error"> {error.message} </p> } </form> </> ); } function submitForm(answer) { // Pretend it's hitting the network. return new Promise((resolve, reject) => { setTimeout(() => { let shouldError = answer.toLowerCase() !== 'lima' if (shouldError) { reject(new Error('Good guess but a wrong answer. Try again!')); } else { resolve(); } }, 1500); }); }
Готовы изучить эту тему?
Прочитайте Реагирование на ввод с помощью состояния, чтобы узнать, как подходить к взаимодействию с точки зрения управления состоянием.
Читать далееВыбор структуры состояния
Правильная структура состояния может повлиять на то, будет ли компонент приятным для изменения и отладки или станет постоянным источником ошибок. Самый важный принцип заключается в том, что состояние не должно содержать избыточной или дублированной информации. Если есть ненужное состояние, легко забыть обновить его и ввести ошибки!
Например, у этой формы есть переменная состояния fullName:
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> </> ); }
Вы можете удалить её и упростить код, вычисляя fullName во время отрисовки компонента:
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> </> ); }
Это может показаться небольшой правкой, но многие ошибки в приложениях React исправляются именно так.
Готовы изучить эту тему?
Прочитайте Выбор структуры состояния, чтобы узнать, как спроектировать форму состояния, чтобы избежать ошибок.
Читать далееОбмен состоянием между компонентами
Иногда вы хотите, чтобы состояние двух компонентов всегда изменялось вместе. Для этого удалите состояние из обоих компонентов, переместите его в ближайшего общего родителя и передайте его потомкам через свойства. Это известно как «поднятие состояния вверх», и это одна из самых распространённых операций при работе с кодом React.
В этом примере только один блок должен быть активен в одно время. Для достижения этого вместо того, чтобы хранить активное состояние внутри каждого отдельного блока, родительский компонент хранит состояние и определяет свойства для своих дочерних элементов.
import { useState } from 'react'; export default function Accordion() { const [activeIndex, setActiveIndex] = useState(0); return ( <> <h2>Almaty, Kazakhstan</h2> <Panel title="About" isActive={activeIndex === 0} onShow={() => setActiveIndex(0)} > With a population of about 2 million, Almaty is Kazakhstan's largest city. From 1929 to 1997, it was its capital city. </Panel> <Panel title="Etymology" isActive={activeIndex === 1} onShow={() => setActiveIndex(1)} > The name comes from <span lang="kk-KZ">алма</span>, the Kazakh word for "apple" and is often translated as "full of apples". In fact, the region surrounding Almaty is thought to be the ancestral home of the apple, and the wild <i lang="la">Malus sieversii</i> is considered a likely candidate for the ancestor of the modern domestic apple. </Panel> </> ); } function Panel({ title, children, isActive, onShow }) { return ( <section className="panel"> <h3>{title}</h3> {isActive ? ( <p>{children}</p> ) : ( <button onClick={onShow}> Show </button> )} </section> ); }
Готовы изучить эту тему?
Прочитайте Обмен состоянием между компонентами, чтобы узнать, как поднимать состояние вверх и синхронизировать компоненты.
Читать далееСохранение и сброс состояния
При повторной отрисовке компонента React необходимо решить, какие части дерева сохранить (и обновить), а какие отбросить или пересоздать с нуля. В большинстве случаев автоматическое поведение React работает достаточно хорошо. По умолчанию React сохраняет части дерева, которые «соответствуют» дереву ранее отрисованного компонента.
Однако иногда этого не требуется. В этом приложении для чата ввод сообщения и затем изменение получателя не сбрасывает поле ввода. Это может заставить пользователя случайно отправить сообщение не тому человеку:
import { useState } from 'react'; import Chat from './Chat.js'; import ContactList from './ContactList.js'; export default function Messenger() { const [to, setTo] = useState(contacts[0]); return ( <div> <ContactList contacts={contacts} selectedContact={to} onSelect={contact => setTo(contact)} /> <Chat contact={to} /> </div> ) } const contacts = [ { name: 'Taylor', email: 'taylor@mail.com' }, { name: 'Alice', email: 'alice@mail.com' }, { name: 'Bob', email: 'bob@mail.com' } ];
React позволяет переопределить стандартное поведение и принудительно сбросить состояние компонента, передав ему другое key, например, <Chat key={email} />. Это говорит React о том, что если получатель отличается, он должен рассматриваться как разный Chat компонент, который необходимо пересоздать с нуля с новыми данными (и пользовательским интерфейсом, например, полями ввода). Теперь при переключении между получателями поле ввода сбрасывается — даже если вы рендерите тот же компонент.
import { useState } from 'react'; import Chat from './Chat.js'; import ContactList from './ContactList.js'; export default function Messenger() { const [to, setTo] = useState(contacts[0]); return ( <div> <ContactList contacts={contacts} selectedContact={to} onSelect={contact => setTo(contact)} /> <Chat key={to.email} contact={to} /> </div> ) } const contacts = [ { name: 'Taylor', email: 'taylor@mail.com' }, { name: 'Alice', email: 'alice@mail.com' }, { name: 'Bob', email: 'bob@mail.com' } ];
Готовы изучить эту тему?
Прочитайте Сохранение и сброс состояния, чтобы узнать о жизненном цикле состояния и как им управлять.
Читать далееИзвлечение логики состояния в редьюсер
Компоненты с множеством обновлений состояния, распределённых по многим обработчикам событий, могут стать непосильной задачей. В этих случаях вы можете консолидировать всю логику обновления состояния вне компонента в одну функцию, называемую «редусёр». Обработчики событий становятся компактными, так как они только определяют пользовательские «действия». В нижней части файла функция редьюсера определяет, как состояние должно обновляться в ответ на каждое действие!
import { useReducer } from 'react'; import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; export default function TaskApp() { const [tasks, dispatch] = useReducer( tasksReducer, initialTasks ); function handleAddTask(text) { dispatch({ type: 'added', id: nextId++, text: text, }); } function handleChangeTask(task) { dispatch({ type: 'changed', task: task }); } function handleDeleteTask(taskId) { dispatch({ type: 'deleted', id: taskId }); } return ( <> <h1>Prague itinerary</h1> <AddTask onAddTask={handleAddTask} /> <TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} /> </> ); } function tasksReducer(tasks, action) { switch (action.type) { case 'added': { return [...tasks, { id: action.id, text: action.text, done: false }]; } case 'changed': { return tasks.map(t => { if (t.id === action.task.id) { return action.task; } else { return t; } }); } case 'deleted': { return tasks.filter(t => t.id !== action.id); } default: { throw Error('Unknown action: ' + action.type); } } } let nextId = 3; const initialTasks = [ { id: 0, text: 'Visit Kafka Museum', done: true }, { id: 1, text: 'Watch a puppet show', done: false }, { id: 2, text: 'Lennon Wall pic', done: false } ];
Готовы изучить эту тему?
Прочитайте Извлечение логики состояния в редьюсер, чтобы узнать, как консолидировать логику в функции редьюсера.
Читать далееПередача данных глубоко с помощью контекста
Обычно вы передаёте информацию из родительского компонента в дочерний компонент через свойства. Но передача свойств может стать неудобной, если вам нужно передать какое-то свойство через много компонентов или если многим компонентам нужна одинаковая информация. Контекст позволяет родительскому компоненту сделать информацию доступной для любого компонента в дереве ниже него — независимо от того, насколько он глубоко — без явной передачи через свойства.
Здесь компонент Heading определяет свой уровень заголовка, «спрашивая» ближайший Section о его уровне. Каждый Section отслеживает свой собственный уровень, спрашивая у родительского Section и добавляя к нему единицу. Каждый Section предоставляет информацию всем компонентам ниже него без передачи свойств — он делает это через контекст.
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section> <Heading>Title</Heading> <Section> <Heading>Heading</Heading> <Heading>Heading</Heading> <Heading>Heading</Heading> <Section> <Heading>Sub-heading</Heading> <Heading>Sub-heading</Heading> <Heading>Sub-heading</Heading> <Section> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-heading</Heading> </Section> </Section> </Section> </Section> ); }
Готовы изучить эту тему?
Прочитайте Передача данных глубоко с помощью контекста, чтобы узнать о контексте как альтернативе передаче свойств.
Читать далееМасштабирование с редьюсером и контекстом
Редьюсеры позволяют консолидировать логику обновления состояния компонента. Контекст позволяет передавать информацию глубоко в другие компоненты. Вы можете объединить редьюсеры и контекст для управления состоянием сложного экрана.
С этим подходом родительский компонент со сложным состоянием управляет им с помощью редьюсера. Другие компоненты где угодно глубоко в дереве могут читать его состояние через контекст. Они также могут отправлять действия для обновления этого состояния.
import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; import { TasksProvider } from './TasksContext.js'; export default function TaskApp() { return ( <TasksProvider> <h1>Day off in Kyoto</h1> <AddTask /> <TaskList /> </TasksProvider> ); }
Готовы изучить эту тему?
Прочитайте Масштабирование с редьюсером и контекстом, чтобы узнать, как управление состоянием масштабируется в растущем приложении.
Читать далееЧто дальше?
Перейдите к Реагирование на ввод с помощью состояния, чтобы начать чтение этой главы по страницам!
Или, если вы уже знакомы с этими темами, почему бы не прочитать о Выходных стратегиях?
© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://18.react.dev/learn/managing-state