Управление состоянием
По мере роста вашего приложения становится все важнее продуманно подходить к организации состояния и потокам данных между компонентами. Избыточное или дублирующееся состояние — частая причина ошибок. В этой главе вы узнаете, как правильно структурировать состояние, как обеспечить удобство обслуживания логики обновления состояния и как совместно использовать состояние между удаленными компонентами.
Вы узнаете
- Как рассматривать изменения интерфейса как изменения состояния
- Как правильно структурировать состояние
- Как «поднять состояние» для совместного использования между компонентами
- Как контролировать, сохраняется ли состояние или сбрасывается
- Как объединить сложную логику состояния в одной функции
- Как передавать информацию без «prop drilling»
- Как масштабировать управление состоянием по мере роста приложения
Реагирование на ввод с помощью состояния
В 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 исправляются именно таким образом.
Совместное использование состояния между компонентами
Иногда требуется, чтобы состояние двух компонентов всегда изменялось синхронно. Для этого удалите состояние из обоих компонентов, переместите его в ближайший общий родительский компонент, а затем передайте им через props. Это называется «подъемом состояния» (lifting state up) и является одной из самых распространенных операций при написании кода React.
В этом примере одновременно должна быть активна только одна панель. Чтобы этого добиться, вместо того, чтобы хранить состояние активности внутри каждой отдельной панели, родительский компонент хранит состояние и задает props для своих дочерних элементов.
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 }];Глубокая передача данных с помощью context
Обычно информация передается из родительского компонента в дочерний через props. Однако передача props может стать неудобной, если вам нужно передать какой-то prop через множество компонентов или если многим компонентам требуется одна и та же информация. Контекст позволяет родительскому компоненту сделать некоторую информацию доступной для любого компонента в дереве ниже него — независимо от глубины — без явной передачи через props.
Здесь компонент Heading определяет свой уровень заголовка, «спрашивая» ближайший Section о его уровне. Каждый Section отслеживает свой собственный уровень, запрашивая родительский Section и прибавляя к нему единицу. Каждый Section предоставляет информацию всем компонентам ниже него без передачи props — он делает это через контекст.
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> );}Обновление массивов в state
В JavaScript массивы являются изменяемыми, но при хранении их в state следует рассматривать как неизменяемые. Как и в случае с объектами, если вы хотите обновить массив, хранящийся в state, вам нужно создать новый (или сделать копию существующего), а затем настроить state так, чтобы он использовал новый массив.
Реагирование на ввод с помощью состояний
React предоставляет декларативный подход к управлению пользовательским интерфейсом. Вместо того чтобы напрямую манипулировать отдельными элементами интерфейса, вы описываете различные состояния, в которых может находиться ваш компонент, и переключаетесь между ними в ответ на действия пользователя. Это похоже на то, как дизайнеры подходят к созданию пользовательского интерфейса.