08 апреля 2020

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

Frontend-разработчик в Foquz. https://www.cat-in-web.ru/
Забавный рассказ с интерактивными иллюстрациями (и соответствующим кодом) о ментальной модели, которая поможет новичкам в React запомнить, как правильно с ним работать.
Суперменом может стать каждый: разделяем дизайн и данные в React

Вашему вниманию предлагается принципиальная схема построения пользовательского интерфейса средствами библиотеки 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 для начинающих:

Источники

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик C++
Москва, по итогам собеседования

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