JavaScript в фигурных скобках в JSX в React компонентах

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

JSX позволяет вам писать HTML-подобную разметку внутри файла JavaScript, сохраняя логику рендеринга и содержимое в одном месте. Иногда вам может понадобиться добавить немного логики JavaScript или сослаться на динамическое свойство внутри этой разметки. В этой ситуации вы можете использовать фигурные скобки в JSX, чтобы добавить в них JavaScript.

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

Передача строк в кавычках

Когда нужно передать строковый атрибут в JSX, нужно заключить его в одинарные или двойные кавычки:
export default function Avatar() {
  return (
    <img
      className="avatar"
      src="http://example.com/userpic.jpg"
      alt="User Name"
    />
  );
}
Здесь "http://example.com/userpic.jpg" и "User Name" передаются как строки. Но что, если вы хотите динамически указать src или alt? Вы можете использовать JavaScript значение, заменив " и " на { и }:
export default function Avatar() {
  const avatar = 'http://example.com/userpic.jpg';
  const description = 'User Name';
  return <img className="avatar" src={avatar} alt={description} />;
}
Обратите внимание на разницу между className="avatar", который указывает имя CSS класса "avatar", и src={avatar}, который считывает значение переменной JavaScript с именем avatar. Это потому, что фигурные скобки позволяют вам работать с JavaScript прямо в разметке.

Использование фигурных скобок: JavaScript в разметке

JSX — это особый способ написания JavaScript. Это означает, что внутри него можно использовать JavaScript — с помощью фигурных скобок { }. В приведенном ниже примере сначала объявляется имя, name, а затем оно встраивается в фигурные скобки внутри <h1>:
export default function TodoList() {
  const name = 'User Name';

  return <h1>{name}'s To Do List</h1>;
}
Любое выражение JavaScript будет работать между фигурными скобками, включая вызовы функций, таких как formatDate():
const today = new Date();

function formatDate(date) {
  return new Intl.DateTimeFormat('en-US', { weekday: 'long' }).format(date);
}

export default function TodoList() {
  return <h1>To Do List for {formatDate(today)}</h1>;
}

Где использовать фигурные скобки

Вы можете использовать фигурные скобки только двумя способами внутри JSX:
  1. Как текст непосредственно внутри тега JSX: <h1>{name}'s To Do List</h1> работает, но <{tag}>To Do List</{tag}> работать не будет.
  2. Поскольку атрибуты следуют сразу после знака =: src={avatar} будут считывать переменную avatar, но src="{avatar}" передаст строку "{avatar}".

Использование двойных фигурных скобок: CSS и другие объекты в JSX

Помимо строк, чисел и других выражений JavaScript, вы даже можете передавать объекты в JSX. Объекты также обозначаются фигурными скобками, например { name: "User Name", itemsCount: 10 }. Следовательно, чтобы передать объект JS в JSX, вы должны заключить объект в другую пару фигурных скобок: person={{ name: "User Name", itemsCount: 10 }}. Вы можете увидеть это со встроенными стилями CSS в JSX. React не требует от вас использования встроенных стилей (классы CSS отлично подходят для большинства случаев). Но когда вам нужен встроенный стиль, нужно передать объект атрибуту стиля:
export default function TodoList() {
  return (
    <ul
      style={{
        backgroundColor: 'black',
        color: 'pink',
      }}
    >
      <li>Item 1</li>
      <li>Item 2</li>
      <li>Item 3</li>
    </ul>
  );
}
Встроенные (inline) стили записываются в camelCase. Например, HTML <ul style="background-color: black"> в компоненте будет записан как <ul style={{ backgroundColor: 'black' }}>.

Другие примеры JavaScript объектов в фигурных скобках

Вы можете переместить несколько выражений в один объект и ссылаться на них в JSX внутри фигурных скобок:
const person = {
  name: 'User Name',
  theme: {
    backgroundColor: 'black',
    color: 'pink',
  },
};

export default function TodoList() {
  return (
    <div style={person.theme}>
      <h1>{person.name}'s Todos</h1>
      <img
        className="avatar"
        src="https://example.com/userpic.jpg"
        alt="userpic"
      />
      <ul>
        <li>Item 1</li>
        <li>Item 2</li>
        <li>Item 3</li>
      </ul>
    </div>
  );
}
В этом примере объект JavaScript person содержит строку имени и объект темы:
const person = {
  name: 'User Name',
  theme: {
    backgroundColor: 'black',
    color: 'pink',
  },
};
Компонент может использовать эти значения из person следующим образом:
<div style={person.theme}>
  <h1>{person.name}'s Todos</h1>
JSX очень минималистичен как язык шаблонов, потому что он позволяет вам организовывать данные и логику с помощью JavaScript.

Резюме

Теперь вы знаете почти все о JSX:
  • Атрибуты JSX внутри кавычек передаются как строки.
  • Фигурные скобки позволяют добавить в разметку логику и JavaScript переменные.
  • Они работают внутри содержимого тега JSX или сразу после = в атрибутах.
  • {{ и }} не являются специальным синтаксисом: это объект JavaScript, спрятанный внутри фигурных скобок JSX.

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

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