Spec-Zone.ru › React

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

Промежуточный

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

В этой главе

  • Как рассматривать изменения интерфейса как изменения состояния
  • Как правильно структурировать состояние
  • Как «поднять состояние вверх» для его совместного использования между компонентами
  • Как контролировать сохранение или сброс состояния
  • Как консолидировать сложную логику состояния в функции
  • Как передавать информацию без «спускания props»
  • Как масштабировать управление состоянием по мере роста приложения

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

В 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. Это известно как «поднятие состояния вверх», и это одна из самых распространенных вещей, которые вы будете делать, работая с 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 }
];

Готовы изучить эту тему?

Прочитайте Вынесение логики состояния в редьюсер, чтобы узнать, как консолидировать логику в функции редьюсера.

Читать далее

Передача данных на глубину с помощью контекста

Обычно вы передаете информацию от родительского компонента до дочернего компонента через 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>
  );
}

Готовы изучить эту тему?

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

Читать далее

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

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

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

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://react.dev/learn/managing-state

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API