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

2 года назад·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. Подключите обработчики событий для установки состояния.

Как открыть ссылку в новой вкладке — объяснение HTML target blank

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

При разработке веб-сайта хочется, чтобы пользователь щелкнув ссылку, открыл ее в новой вкладке браузера. Но как это сделать в HTML?

В этой статье мы рассмотрим, как использовать атрибут target="_blank" на примерах кода, и рассмотрим случае, когда вам следует рассмотреть возможность использования этого атрибута.

Как открыть новую вкладку браузера с помощью target="_blank"?

Атрибут target="_blank" используется внутри открывающего тега <a> следующим образом:
<a href="website-link-goes-here" target="_blank">
Когда пользователь нажимает на ссылку, на этой странице автоматически открывается новая вкладка браузера. Если не добавлять атрибут target="_blank" то поведение по умолчанию заключалось бы в том, что ссылка открылась бы в текущей вкладке браузера.

Что такое ключевое слово noopener?

Ключевое слово noopener в атрибуте rel используется в основном из соображений безопасности, чтобы злоумышленники не могли совершать манипуляции с исходной веб-страницей через свойство Window.opener. Если злоумышленник получит доступ к вашему объекту window, он может перенаправить вашу страницу на вредоносный URL-адрес. Вы можете использовать ключевое слово noopener, чтобы предотвратить возникновение этой проблемы безопасности. Вот как используется ключевое слово noopener:
<a target="_blank" rel="noopener" href="https://example.com">Example</a>

Обновления ключевого слова noopener

В 2021 году было сделано обновление, в котором современные браузеры теперь неявно устанавливают rel=noopener на любую ссылку с атрибутом target=_blank. Как вы можете видеть в этой таблице Can I use, ключевое слово noopener поддерживается большинством браузеров, кроме Internet Explorer 11. Даже с этим обновлением многие разработчики по-прежнему используют rel=noopener для ссылок, использующих атрибут target=_blank.

Стоит ли постоянно использовать атрибут target="_blank"?

Когда пользователи нажимают на ссылку, поведение по умолчанию заключается в том, чтобы эта ссылка открывалась на текущей странице, на которой они находятся, не открывая новую вкладку браузера. Вы должны хорошо подумать о том, в каких случаях использование атрибута target="_blank" будет подходящим. Хорошим примером может быть ситуация, когда пользователь работает над страницей и не хочет покидать эту страницу, если нажмет на ссылку.

Итоги

Вы можете использовать target="_blank" если хотите, чтобы ваши пользователи нажимали на ссылку, открывающую новую вкладку браузера. Атрибут target="_blank" используется внутри открывающего тега <a> следующим образом.
<a href="website-link-goes-here" target="_blank">
Ключевое слово noopener в атрибуте rel добавляется, чтобы злоумышленники не могли работать с исходной веб-страницей через свойство Window.opener.