HOW TO React

Управление состоянием

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

Реагирование на ввод с помощью состояния

В 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);  });}
Preview

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

Правильная структуризация состояния может стать решающим фактором между компонентом, который приятно модифицировать и отлаживать, и тем, который является постоянным источником ошибок. Самый важный принцип заключается в том, что состояние не должно содержать избыточной или дублирующейся информации. Если есть ненужное состояние, легко забыть его обновить и внести ошибки!

Например, в этой форме есть избыточная переменная состояния 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>    </>  );}
Preview

Вы можете удалить его и упростить код, вычисляя 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>    </>  );}
Preview

Это может показаться небольшим изменением, но многие ошибки в приложениях 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>  );}
Preview

Сохранение и сброс состояния

Когда вы повторно рендерите компонент, 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' }];
Preview

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' }];
Preview

Выделение логики состояния в редуктор

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

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 }];
Preview

Глубокая передача данных с помощью 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>  );}
Preview

Масштабирование с помощью редуктора и контекста

Редукторы позволяют консолидировать логику обновления состояния компонента. Контекст позволяет передавать информацию глубоко вниз к другим компонентам. Вы можете комбинировать редукторы и контекст для управления состоянием сложного экрана.

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

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>  );}
Preview

On this page