Обработка ввода с состоянием в React

9 месяцев назад·8 мин. на чтение

React использует декларативный способ управления пользовательским интерфейсом. Вместо непосредственного управления отдельными частями пользовательского интерфейса вы описываете различные состояния, в которых может находиться ваш компонент, и переключаетесь между ними в ответ на действия пользователя. Это похоже на то, как дизайнеры думают о пользовательском интерфейсе.

Содержание туториала по React В этой части руководства по React рассмотрим как реагировать на события ввода. React использует декларативный способ управления пользовательским интерфейсом. Вместо непосредственного управления отдельными частями пользовательского интерфейса вы описываете различные состояния, в которых может находиться ваш компонент, и переключаетесь между ними в ответ на действия пользователя. Это похоже на то, как дизайнеры думают о пользовательском интерфейсе.

Сравнение декларативного пользовательского интерфейса с императивным

Когда вы проектируете взаимодействия с пользовательским интерфейсом, вы, вероятно, думаете о том, как пользовательский интерфейс изменяется в ответ на действия пользователя. Рассмотрим форму, которая позволяет пользователю отправить ответ:
  • Когда вы вводите что-то в форму, кнопка «Отправить» становится активной.
  • Когда вы нажимаете «Отправить», и форма, и кнопка блокируются, и появляется анимация ожидания.
  • Если сетевой запрос выполнен успешно, форма скрывается и появляется сообщение «Спасибо».
  • Если сетевой запрос завершается неудачно, появляется сообщение об ошибке, и форма снова становится доступной.
В императивном программировании вышеизложенное напрямую соответствует тому, как вы реализуете взаимодействие. Вы должны написать точные инструкции для управления пользовательским интерфейсом в зависимости от того, что только что произошло. Вот еще один способ подумать об этом: представьте, что вы едете рядом в машине в качестве пассажира и говорите водителю как ехать и где сворачивать и в какую сторону.
Водитель не знают, куда вы хотите поехать, он просто следуют вашим командам. (И если вы ошибетесь, вы окажетесь не в том месте) Это называется императивным подходом, потому что вы должны «командовать» каждым элементом, от счетчика до кнопки, сообщая компьютеру, как обновить пользовательский интерфейс. В этом примере императивного программирования пользовательского интерфейса форма создается без использования React. Он использует встроенный в браузер DOM:
async function handleFormSubmit(e) {
  e.preventDefault();
  disable(textarea);
  disable(button);
  show(loadingMessage);
  hide(errorMessage);
  try {
    await submitForm(textarea.value);
    show(successMessage);
    hide(form);
  } catch (err) {
    show(errorMessage);
    errorMessage.textContent = err.message;
  } finally {
    hide(loadingMessage);
    enable(textarea);
    enable(button);
  }
}

function handleTextareaChange() {
  if (textarea.value.length === 0) {
    disable(button);
  } else {
    enable(button);
  }
}

function hide(el) {
  el.style.display = 'none';
}

function show(el) {
  el.style.display = '';
}

function enable(el) {
  el.disabled = false;
}

function disable(el) {
  el.disabled = true;
}

function submitForm(answer) {
  // Pretend it's hitting the network.
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (answer.toLowerCase() == 'istanbul') {
        resolve();
      } else {
        reject(new Error('Good guess but a wrong answer. Try again!'));
      }
    }, 1500);
  });
}

let form = document.getElementById('form');
let textarea = document.getElementById('textarea');
let button = document.getElementById('button');
let loadingMessage = document.getElementById('loading');
let errorMessage = document.getElementById('error');
let successMessage = document.getElementById('success');
form.onsubmit = handleFormSubmit;
textarea.oninput = handleTextareaChange;
<!-- index.html -->

<form id="form">
  <h2>City quiz</h2>
  <p>What city is located on two continents?</p>
  <textarea id="textarea"></textarea>
  <br />
  <button id="button" disabled>Submit</button>
  <p id="loading" style="display: none">Loading...</p>
  <p id="error" style="display: none; color: red;"></p>
</form>
<h1 id="success" style="display: none">That's right!</h1>

<style>
  * {
    box-sizing: border-box;
  }
  body {
    font-family: sans-serif;
    margin: 20px;
    padding: 0;
  }
</style>
Императивное управление пользовательским интерфейсом работает достаточно хорошо для отдельных примеров, но в более сложных системах управлять им становится экспоненциально сложнее. Представьте, что вы обновляете страницу, полную различных форм, подобных этой. Добавление нового элемента пользовательского интерфейса или нового взаимодействия потребует тщательной проверки всего существующего кода, чтобы убедиться, что вы не внесли ошибку (например, забыли что-то показать или скрыть). React был создан, чтобы решить эту проблему. В React вы не управляете пользовательским интерфейсом напрямую — это означает, что вы не включаете, не отключаете, не показываете и не скрываете компоненты напрямую. Вместо этого вы объявляете, что хотите показать, а React выясняет, как обновить пользовательский интерфейс. Подумайте о том, чтобы сесть в такси и сказать водителю, куда вы хотите ехать, вместо того, чтобы указывать ему, где именно повернуть. Доставить вас туда — работа водителя, и они могут даже знать некоторые короткие пути, о которых вы не подумали.

Декларативный подход к пользовательскому интерфейсу

Вы видели, как императивно реализовать форму выше. Чтобы лучше понять, как мыслить в React, выполним реализацию этого пользовательского интерфейса в React:
  1. Определить различные визуальные состояния компонента
  2. Определить, что вызывает эти изменения состояния
  3. Представить состояние в памяти, используя useState
  4. Удалить все несущественные переменные состояния
  5. Подключить обработчики событий для установки состояния

Шаг 1. Определить различные визуальные состояния компонента

В информатике вы могли слышать о «машине состояний» (state machine, стейт машина, конечный автомат), находящейся в одном из нескольких «состояний». Если вы работаете с дизайнером, возможно, вы видели мокапы для разных «визуальных состояний». React стоит на стыке дизайна и информатики, поэтому обе эти идеи являются источниками вдохновения. Во-первых, вам нужно визуализировать все различные «состояния» пользовательского интерфейса, которые может видеть пользователь:
  • Пусто (empty): в форме отключена кнопка «Отправить».
  • Ввод (typing): Форма имеет активную кнопку «Отправить».
  • Отправка (submitting): Форма полностью отключена. Показана анимация ожидания.
  • Успех (success): вместо формы отображается сообщение «Спасибо».
  • Ошибка (error): то же, что и состояние ввода, но с дополнительным сообщением об ошибке.
Так же, как дизайнер, вы захотите создавать «макеты» для различных состояний, прежде чем добавлять логику. Например, вот макет только для визуальной части формы. Этот макет управляется пропсом, называемым status, со значением по умолчанию 'empty':
export default function Form({
  // Try 'submitting', 'error', 'success':
  status = 'empty',
}) {
  if (status === 'success') {
    return <h1>That's right!</h1>;
  }
  return (
    <>
      <h2>City quiz</h2>
      <p>
        In which city is there a billboard that turns air into drinkable water?
      </p>
      <form>
        <textarea disabled={status === 'submitting'} />
        <br />
        <button disabled={status === 'empty' || status === 'submitting'}>
          Submit
        </button>
        {status === 'error' && (
          <p className="Error">Good guess but a wrong answer. Try again!</p>
        )}
      </form>
    </>
  );
}
Как отобразить множество визуальных состояний компонента одновременно? Если у компонента много визуальных состояний, как у компонента Form выше, будет удобно показать их все на одной странице. Такие страницы часто называют «живыми руководствами по стилю» (living styleguides) или storybooks.

Шаг 2. Определить, что вызывает эти изменения состояния

Вы можете запускать обновления состояния в ответ на два типа входных данных:
  • Ввод пользователя, например нажатие кнопки, ввод в поле, переход по ссылке.
  • Входные данные компьютера, такие как получение сетевого ответа, завершение тайм-аута, загрузка изображения.
В обоих случаях необходимо установить переменные состояния для обновления пользовательского интерфейса. Для формы, которую вы разрабатываете, вам нужно будет изменить состояние в ответ на несколько разных входных данных:
  • Изменение ввода текста (пользователем) должно переключить его из пустого (empty) состояния в состояние ввода (typing) или обратно, в зависимости от того, пусто текстовое поле или нет.
  • Нажатие (пользователем) кнопки «Отправить» должно переключить ее в состояние «Отправка» (submitting).
  • Успешный сетевой ответ (компьютер) должен перевести его в состояние успеха (success).
  • Неудачный сетевой ответ (компьютер) должен перевести его в состояние (error) с соответствующим сообщением об ошибке.
Обратите внимание, что для ввода пользователя часто требуются обработчики событий. Чтобы визуализировать этот поток, попробуйте нарисовать каждое состояние на бумаге в виде помеченного круга, а каждое изменение между двумя состояниями — в виде стрелки. Таким образом можно набросать множество потоков и разобраться с ошибками задолго до реализации.

Шаг 3: Представить состояние в памяти, используя useState

Далее вам нужно представить визуальные состояния вашего компонента в памяти с помощью useState. Ключевым моментом является простота: каждая часть состояния является «движущейся частью», и вам нужно как можно меньше «движущихся частей». Большая сложность приводит к большему количеству ошибок. Начните с состояния, которое обязательно должно быть. Например, вам нужно сохранить ответ для ввода и ошибку (если она существует) для сохранения последней ошибки:
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
Затем вам понадобится переменная состояния, представляющая, какое из визуальных состояний, описанных ранее, вы хотите отобразить. Обычно существует более одного способа представить это в памяти, поэтому вам нужно поэкспериментировать с ним. Если вы изо всех сил пытаетесь сразу придумать лучший способ, начните с добавления достаточного количества состояний, чтобы быть уверенным, что охватываются все возможные визуальные состояния:
const [isEmpty, setIsEmpty] = useState(true);
const [isTyping, setIsTyping] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [isError, setIsError] = useState(false);
Ваша первая идея, скорее всего, не будет лучшей, но это нормально — рефакторинг — это часть процесса.

Шаг 4. Удалить все несущественные переменные состояния

Вы хотите избежать дублирования в содержании состояния, чтобы отслеживать только то, что необходимо. Потратив немного времени на рефакторинг вашей структуры состояния, вы облегчите понимание своих компонентов, уменьшите дублирование и предотвратите непреднамеренное значение. Ваша цель — предотвратить случаи, когда состояние в памяти не представляет никакого допустимого пользовательского интерфейса, который вы хотели бы видеть пользователю. (Например, вы никогда не хотите показывать сообщение об ошибке и одновременно отключать ввод, иначе пользователь не сможет исправить ошибку.) Вот несколько вопросов, которые вы можете задать про свои переменные состояния:
  • Вызывает ли это состояние парадокс? Например, isTyping и isSubmitting не могут одновременно быть истинными. Парадокс обычно означает, что состояние недостаточно ограничено. Существует четыре возможных комбинации двух логических значений, но только три соответствуют действительным состояниям. Чтобы удалить невозможное состояние, вы можете объединить их в состояние, которое должно иметь одно из трех значений: «ввод», «отправка» или «успех».
  • Доступна ли та же информация в другой переменной состояния? Еще один парадокс: isEmpty и isTyping не могут быть истинными одновременно. Делая их отдельными переменными состояния, вы рискуете рассинхронизировать их и вызвать ошибки. К счастью, вы можете удалить isEmpty и вместо этого проверить answer.length === 0.
  • Можно ли получить ту же информацию из инверсии другой переменной состояния? isError не нужен, потому что вместо этого вы можете проверить error !== null.
После этой очистки у вас осталось 3 (вместо 7!) основных переменных состояния:
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing'); // 'typing', 'submitting', or 'success'
Вы знаете, что они необходимы, потому что вы не можете удалить ни один из них, не нарушив функциональность.

Как устраненить «невозможные» состояния с помощью редьюсера

Эти три переменные являются достаточно хорошим представлением состояния этой формы. Тем не менее, есть еще некоторые промежуточные состояния, которые не имеют полного смысла. Например, ненулевая ошибка не имеет смысла, когда статус равен «успех». Чтобы точнее смоделировать состояние, вы можете извлечь его в редьюсер. Редьюсеры позволяют объединить несколько переменных состояния в один объект и объединить всю связанную логику.

Шаг 5: Подключить обработчики событий для установки состояния

Наконец, создайте обработчики событий для установки переменных состояния. Ниже приведена окончательная форма со всеми подключенными обработчиками событий:
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);
  });
}
Хотя этот код длиннее исходного императивного примера, он намного менее подвержен ошибкам. Выражение всех взаимодействий в виде изменений состояния позволяет позже вводить новые визуальные состояния, не нарушая существующие. Это также позволяет вам изменить то, что должно отображаться в каждом состоянии, не меняя логику самого взаимодействия.

Резюме

  • Декларативное программирование означает описание пользовательского интерфейса для каждого визуального состояния, а не микроуправление пользовательским интерфейсом (императивное).
  • При разработке компонента:
    1. Определите все его визуальные состояния.
    2. Определите триггеры пользователя и компьютера для изменения состояния.
    3. Смоделируйте состояние с помощью useState.
    4. Удалите несущественное состояние, чтобы избежать ошибок и парадоксов.
    5. Подключите обработчики событий для установки состояния.

Полное руководство по React

год назад·3 мин. на чтение

Это руководство поможет плавно войти в Реакт разработку и понять как использовать react.

Мы начнем с самых базовых концепций. Рассмотрим различные варианты добавления ReactJS на сайт и создания React проекта. Создадим первые компоненты и шаг за шагом будем подробно рассматривать все части компонента - JSX, события, стили, хуки и т.д. Данный react туториал на русском разделен на удобные части, каждая из которых включает подробное объяснение и простые примеры. Этот гайд подойдет и для начинающих react разработчиков. Также весь API собран в удобный справочник, в котором можно найти подробное объяснение конкретных функций React.

Установка

В этом разделе полного руководства по Реакт собраны все варианты установки React. Можно создать проект на React с нуля или постепенно внедрять его в существующее приложение. Также узнаем как настроить базовый набор инструментов на компьютере для разработки и отладки React приложения.

Интерфейс пользователя

В этой части руководства по React мы напишем первый React компонент. React компоненты - это переиспользуемые элементы пользовательского интерфейса. В приложении React каждая часть пользовательского интерфейса является компонентом.

Добавление интерактивности

Эта часть руководства по React посвящена тому как обрабатывать действия пользователя. Некоторые элементы на экране обновляются в ответ на действия пользователя. Например, клик по галерее изображений переключает активное изображение. В React данные, которые меняются со временем, называются состоянием. Вы можете добавить состояние к любому компоненту и обновить его по мере необходимости. В этой части курса по Реакт вы узнаете, как писать компоненты, которые обрабатывают взаимодействия, обновляют свое состояние и отображают разные выходные данные с течением времени.

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

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

Продвинутые темы

Некоторым из ваших компонентов может потребоваться управление и синхронизация с системами вне React. Например, вам может потребоваться сфокусировать ввод с помощью API браузера, воспроизвести и приостановить видеопроигрыватель, реализованный без React, или подключиться и прослушать сообщения с удаленного сервера. В этой части курса по Реакт вы узнаете о лозейках, позволяющих «выйти за пределы» React и подключиться к внешним системам. Большая часть логики вашего приложения и потока данных не должны полагаться на эти функции.
  • Доступ к данным через рефы
  • Управление DOM с помощью рефов
  • Синхронизация с помощью эффектов
  • Вам может не понадобиться эффект
  • Жизненный цикл реактивных эффектов
  • Отделение событий от эффектов
  • Удаление лишних зависимостей эффектов
  • Переиспользование логики с кастомными хуками