furry.cat 08 апреля 2020

Суперменом может стать каждый: разделяем дизайн и данные в React

Забавный рассказ с интерактивными иллюстрациями (и соответствующим кодом) о ментальной модели, которая поможет новичкам в React запомнить, как правильно с ним работать.
13027

Вашему вниманию предлагается принципиальная схема построения пользовательского интерфейса средствами библиотеки React:

Ментальная модель создания UI на React
Ментальная модель создания UI на React

В схеме два этапа:

  1. Пишем HTML-код.
  2. Вырезаем в нём дырки.

Что? Вырезаем дырки? Именно! Такие, чтобы было видно, что там находится сзади – вот так:

Прорезанная в HTML дырка
Прорезанная в HTML дырка

Получается вроде фотоплаката с красивой картинкой и отверстием, куда вы просовываете голову, чтобы сделать смешной снимок (по-умному такой стенд ещё называется тантамареской).

Конечно, мы не вырезаем «дырки» в коде физически – ножницами, канцелярским ножом или ещё каким-нибудь острым предметом. И в документации React вы такого термина не найдете. Это всего лишь ментальная модель, дающая понять, как всё работает. Особенно, если вы новичок в React.

Мастерами кунг-фу не рождаются

Начнём с самого простого React-приложения – выведем на экран Кунг-фу Панду в виде эмодзи:

        💪🐼👊
    

Компоненты в React выглядят как обычные функции:

        function App() {
  return <div>💪🐼👊</div>
}
    

Всего лишь возвращаем из функции тег HTML с нужным контентом: обычная JS-функция, обычный return, обычный HTML (есть некоторые тонкости, но в целом – да, самый обычный). В общем, пока что ничего сложного.

Добавляем динамику

Получившееся приложение не очень функционально – его и приложением-то не назвать. Добавим интерактив. Например, будем менять персонажа на Кунг-фу Обезьяну, Кунг-фу Тигрицу или, может быть... Кунг-фу Хрюшку? Ниже представлен интерактивный элемент – кнопки можно нажимать, и наблюдать изменение, а также смотреть HTML и JS-код (по кнопке Babel).

Кунг-фу команда в сборе!

Всё, что мы до сих пор писали в HTML, было как рисунок маркером на картоне – неизменяемо. Как нарисовали, так и осталось. Но что делать, если на этом рисунке нужно поменять пару символов – или подставить чью-то голову? Да просто вырезать дырку в нужном месте – вот так:

        function App() {
  return <div>💪{ }👊</div>
}
    

Теперь в это отверстие можно поместить кого угодно – хоть панду, хоть хрюшку:

        function App() {
  const who = '🐼'
  return <div>💪{ who }👊</div>
}
    

Нужно лишь поменять значение переменной who в коде.

Чтобы менять персонажа динамически, давайте добавим кнопку. Клик по ней будет изменять значение переменной.

        function App() {
  const who = '🐼'
  return (
    <div>
      <div>💪{ who }👊</div>
      <button onClick={
        function() {
          // здесь должна быть логика по изменению who на 🐯
        }
      }>Тигрица</button>
    </div>
  )
}
    

Как нам это сделать? Возможно, вы думаете, что всё просто:

        who = '🐯'
    

Но, к сожалению, в React это не сработает. Мы не можем просто так менять данные (и это, на самом деле защищает нас от многих проблем). React предоставляет специальный способ – локальное состояние компонента:

        function App() {
  const [who, setWho] = React.useState('🐼')  
  return (
    <div>
      <div>💪{ who }👊</div>
      <button onClick={
        function() {
          setWho('🐯')        
        }
      }>Тигрица</button>
    </div>
  )
}
    

Этот код может напугать неподготовленного читателя, но в нем нет ничего сложного. Первая строчка эквивалента следующему фрагменту:

        const result = React.useState('🐼')
const who = result[0]
const setWho = result[1]
    

Метод React.useState() возвращает массив из двух элементов. Первый – это само состояние компонента, а второй – метод для его изменения. Для краткости мы используем синтаксис деструктуризации массива – сразу записываем его элементы в отдельные переменные.

В метод можно передать начальное значение who. Поставим туда Панду 🐼.

Теперь в обработчике клика по кнопке (атрибут onClick) мы вызываем полученную функцию setWho, чтобы изменить состояние на Тигрицу 🐯.

Другими словами, функция setWho выбирает, кто именно будет «фотографироваться».

Функция <code class="inline-code">setWho</code> изменяет состояние компонента
Функция setWho изменяет состояние компонента

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

Эта аналогия отлично демонстрирует важную идею React. Существует два понятия:

  • Общая структура (которая практически всегда остается статической);
  • Данные (которые время от времени изменяются).

Эти понятия должны работать отдельно друг от друга.

Когда мы хотим что-то изменить в React, то почти всегда обращаемся к данным. Мы не изменяем напрямую UI-элементы (узлы DOM). Вместо этого мы изменяем данные, а элементы интерфейса обновляются автоматически.

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

        $('div#who').text('🐯')
    

Но в React мы меняем только данные, а взаимодействие с DOM берет на себя библиотека:

        setWho('🐯')
    

В этом коде нигде не упоминается div или другой DOM-элемент. HTML меняется автоматически, так как мы заранее объяснили React, что и где нужно подставить. Иначе говоря, мы прорезали в HTML отверстие, куда теперь подставляем то, что нам нужно.

        <div>💪{ who }👊</div>
    

Ментальная модель React

Итак, для работы с React мы должны понять несколько концепций:

  1. Мы пишем HTML, чтобы определить статическую структуру пользовательского интерфейса.
  2. Затем мы делаем в нем «дырки» с помощью конструкции { }, чтобы в них можно было разместить динамические данные.
  3. Изменяющиеся данные – это состояние компонента, и для них мы используем специальные методы React.

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

Домашнее задание

Для закрепления знаний попробуйте реализовать вот такое приложение:

Решение здесь.

Берёмся за Input

Чтобы лучше понять описанную ментальную модель React, обратимся к тегу input.

Напишем простой компонент:

        function App() {
  return (
    <div>
      <input type="text" />
    </div>
  )
}
    

Input по своей природе интерактивен – он позволяет пользователю вводить символы и сразу же отображает результат ввода.

Как получить значение?

Сначала добавим кнопку. При клике на нее будем показывать окошко с текстом из поля ввода:

        function App() {
  return (
    <div>
      <input type="text" />
      <button onClick={
        function() {
          // здесь вывод сообщения alert(текст_в_поле_ввода)        
        }
      }>Send</button>
    </div>
  )
}
    

Как получить этот текст из инпута? Если вы подумали, что нужно каким-то образом получить доступ к DOM-элементу, подумайте еще раз! Помните, что мы не должны взаимодействовать с интерфейсом – только с данными!

Вспомните про наш фотоплакат: нужно в статической структуре прорезать дырку { } – но где? В том месте, где находятся изменяющиеся данные – в атрибуте value:

        function App() {
  const [draft, setDraft] = React.useState("")
  return (
    <div>
      <input type="text" value={draft} />
      <button onClick={
        function() {
          // здесь вывод сообщения alert(текст_в_поле_ввода)  
        }
      }>Send</button>
    </div>
  )
}
    

Теперь текст хранится в переменной draft, и мы легко можем его вывести:

        <button onClick={
  function() {
    alert(draft)
  }
}>Send</button>
    

Вопрос вида «какой текст находится в поле ввода?» – неправильный. Мы должны спросить: «какие данные привязаны к этому инпуту?» Или «что находится за этой дыркой?»

Отверстия можно прорезать в контенте тега и в значениях атрибутов. Но не стоит делать это в других местах.

        <input value={text} />          // ✅ Correct
<div>💪{who}👊</div>           // ✅ Correct
<span>💪{who}{action}</span>   // ✅ Correct

<input {attr}="1" />           // ❌ Wrong 
<{tagName} />                  // ❌ Wrong 
    

Как изменить значение?

Добавим другую кнопку, клик на которую должен изменять текст в поле ввода. Как изменить значение инпута?

        function App() {
  const [draft, setDraft] = React.useState("")
  return (
    <div>
      <input type="text" value={draft} />
      <button onClick={
        function() {
          // поместить "😍" в поле ввода
        }
      }>😍</button>
    </div>
  )
}
    

Даже не пытайтесь вновь думать о прямом доступе к DOM элементу!

        $('input').val('😍')
    

Помните – мы работаем с данными.

Уже подготовлено место в значении атрибута value, остается лишь изменить переменную draft – и инпут обновится автоматически. Как это сделать, мы уже знаем.

        setDraft("😍")
    

Можем сделать что-то в духе следующего интерактивного примера:

Это интерактивный элемент – попробуйте.

Как починить пользовательский ввод?

Кнопки работают отлично, но если вы попробуете что-то напечатать в поле ввода, то ничего не выйдет! Попробуйте, если еще не сделали этого. Как нам удалось сломать обычный инпут?

Посмотрим на код ещё раз:

        function App() {
  const [draft, setDraft] = React.useState("")
  return (
    <div>
      <input type="text" value={draft} />
      <button onClick={
        function() {
          setDraft("😍")
        }
      }>😍</button>
    </div>
  )
}
    

Значение инпута чётко определено переменной draft, которая выглядывает из прорезанной нами дырки. Но мы не меняем эту переменную – поэтому и текст в поле не меняется.

Единственный способ изменить его – воспользоваться функцией setDraft. Сейчас мы вызываем её только при клике на кнопку – в атрибуте onClick. И ничего не делаем при вводе символов в инпут. Исправим это досадное недоразумение:

        <input type="text" value={draft} 
  onChange={
    function(event) {
      setDraft(event.target.value)
    }
  }/>
    

Теперь мы отслеживаем событие change – в React оно происходит при любом изменении инпута, а не только после потери им фокуса, это больше похоже на JavaScript событие input.

В качестве первого аргумента обработчик получает объект события (это не совсем привычный нам Event, но все нужные методы и свойства у него есть). Так что мы можем достать значение инпута из event.target.value и обновить состояние компонента.

Контролируем инпут

Итак, элемент input отлично демонстрирует концепцию «данные отдельно от интерфейса».

  1. Мы «привязываем» некоторый текст к инпуту вместо того, чтобы «извлекать» его из DOM-элемента.
  2. Чтобы обновить значение инпута, нужно обновить привязанные к нему данные.
  3. Для хранения и автоматического обновления текста используем состояние React компонента.
  4. Чтобы отслеживать пользовательский ввод, устанавливаем обработчик на событие onChange.

Такой инпут называется контролируемым – ведь от самого элемента тут ничего не зависит, только мы определяем, что в нем будет находиться.

Точно так же мы можем установить полный контроль над textarea, select или другим интерактивным тегом.

Контролируемые компоненты – это не единственно возможный подход для работы с элементами форм (React позволяет работать с DOM узлами напрямую), но в большинстве случаев – это желательный подход.

Мастерами кунг-фу становятся!

Домашнее задание: Кунг-фу Панда наносит удар.

Создайте приложение, как в следующем интерактивном примере (потом по кнопке Babel можете сравнить свой результат с нашим кодом):

Здесь есть ползунок (<input type="range">), при изменении значения которого кулак Панды стремительно увеличивается.

Все, что вам нужно сделать, – прорезать отверстие в правильном месте.

Решение доступно здесь.

Подсказки

  • Выясните, как правильно указать значение атрибута style.
  • Вместо event.target.value используйте event.target.valueAsNumber.
***

У нас есть ещё множество полезных материалов по React для начинающих:

Источники

РУБРИКИ В СТАТЬЕ

МЕРОПРИЯТИЯ

Комментарии 0

ВАКАНСИИ

Unity Tech Lead
по итогам собеседования
Programmer UE4
Краснодар, по итогам собеседования
Frontend разработчик (react native)
по итогам собеседования
Unity 3D developer
по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

BUG