Перейти к содержанию

Философия React

Нам кажется, React — это отличный способ писать большие и быстрые JavaScript-приложения. Он очень хорошо масштабировался для нас в Facebook и Instagram.

Одна из особенностей React — это то, как он предлагает думать о приложениях в процессе их создания. В этом руководстве мы покажем мысленный процесс создания таблицы продуктов с поиском на React.

Начнём с макета

Представьте, что у вас уже есть JSON API и макет дизайна сайта. Вот как он выглядит:

Mockup

Наш JSON API возвращает данные, которые выглядят так:

1
2
3
4
5
6
7
8
[
  {category: "Sporting Goods", price: "$49.99", stocked: true, name: "Football"},
  {category: "Sporting Goods", price: "$9.99", stocked: true, name: "Baseball"},
  {category: "Sporting Goods", price: "$29.99", stocked: false, name: "Basketball"},
  {category: "Electronics", price: "$99.99", stocked: true, name: "iPod Touch"},
  {category: "Electronics", price: "$399.99", stocked: false, name: "iPhone 5"},
  {category: "Electronics", price: "$199.99", stocked: true, name: "Nexus 7"}
];

Шаг 1: Разобьём интерфейс на составляющие

Первое, что нужно сделать — представить границы вокруг каждого компонента (и подкомпонента) в макете и дать им имена. Если вы работаете с дизайнерами, вполне возможно, что они уже как-то называют компоненты — вам стоит пообщаться! Например, слои Photoshop часто подсказывают имена для React-компонентов.

Но как выбрать, что является компонентом, а что нет? Это похоже на то, как вы решаете, надо ли объявить функцию или объект. Можно применить принцип единственной ответственности: каждый компонент по-хорошему должен заниматься какой-то одной задачей. Если функциональность компонента увеличивается с течением времени, его следует разбить на более мелкие подкомпоненты.

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

Component diagram

Здесь мы видим, что наше приложение состоит из пяти различных компонентов. Курсивом выделены данные, которые эти компоненты представляют.

  1. FilterableProductTable (оранжевый): контейнер, содержащий пример целиком
  2. SearchBar (синий): поле пользовательского ввода
  3. ProductTable (зелёный): отображает и фильтрует список данных, основанный на пользовательском вводе
  4. ProductCategoryRow (голубой): наименования категорий
  5. ProductRow (красный): отдельно взятый товар

Обратите внимание, что внутри ProductTable заголовок таблицы ("Name" и "Price") сам по себе отдельным компонентом не является. Отделять его или нет — вопрос личного предпочтения. В данном примере мы решили не придавать этому особого значения и оставить заголовок частью большего компонента ProductTable, так как он является всего лишь малой частью общего списка данных. Тем не менее, если в будущем заголовок пополнится новыми функциями (например, возможностью сортировать товар), имеет смысл извлечь его в самостоятельный компонент ProductTableHeader.

Теперь, когда мы определили компоненты в нашем макете, давайте расположим их согласно иерархии. Компоненты, которые являются частью других компонентов, в иерархии отображаются как дочерние:

  • FilterableProductTable
    • SearchBar
    • ProductTable
      • ProductCategoryRow
      • ProductRow

Шаг 2: Создадим статическую версию в React

Теперь, когда все компоненты расположены в иерархическом порядке, пришло время реализовать наше приложение. Самый лёгкий способ — создать версию, которая использует модель данных и рендерит интерфейс, но не предполагает никакой интерактивности. Разделять эти процессы полезно. Написание статической версии требует много печатания и совсем немного мышления. С другой стороны, создание интерактивности приложения подразумевает более глубокий мыслительный процесс и лишь долю рутинной печати. Мы разберёмся, почему так выходит, позже.

Чтобы построить статическую версию приложения, отображающую модель данных, нам нужно создать компоненты, которые используют другие компоненты и передают данные через пропсы. Пропсы — это способ передачи данных от родителя к потомку. Если вы знакомы с понятием состояния, то для статической версии это как раз то, чего вам использовать не нужно. Состояние подразумевает собой данные, которые меняются со временем — интерактивность. Так как мы работаем над статической версией приложения, нам этого не нужно.

Написание кода можно начать как сверху вниз (с большого FilterableProductTable), так и снизу вверх (с маленького ProductRow). Более простые приложения удобнее начать с компонентов, находящихся выше по иерархии. В более сложных приложениях удобнее в первую очередь создавать и тестировать подкомпоненты.

В конце этого шага у вас на руках появится библиотека повторно используемых компонентов, отображающих вашу модель данных. Так как это статическая версия, компоненты будут иметь только методы render(). Компонент выше по иерархии (FilterableProductTable) будет передавать модель данных через пропсы. Если вы внесёте изменения в базовую модель данных и снова вызовете ReactDOM.render(), то пользовательский интерфейс отразит эти изменения. Вы можете увидеть, как обновляется интерфейс и где следует сделать очередные изменения. Благодаря одностороннему потоку данных (или односторонней привязке), код работает быстро, но остаётся понятным.

Если у вас остались вопросы по выполнению данного шага, обратитесь к документации React.

Небольшое отступление: как пропсы отличаются от состояния

Существует два типа "модели" данных в React: пропсы и состояние. Важно, чтобы вы понимали разницу между ними, в противном случае обратитесь к официальной документации React.

Шаг 3: Определим минимальное (но полноценное) отображение состояния интерфейса

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

Чтобы правильно построить приложение, сначала нужно продумать необходимый набор данных изменяемого состояния. Главное тут следовать принципу разработки DRY: Don't Repeat Yourself (рус. не повторяйся). Определите минимальное количество необходимого состояния, которое нужно вашему приложению, всё остальное вычисляйте при необходимости. Например, если вы создаёте список дел, держите массив пунктов списка под рукой — но не стоит хранить отдельное состояние для количества дел в списке. Если надо отобразить количество элементов, просто используйте длину существующего массива.

Давайте перечислим все данные в нашем приложении. У нас есть:

  • Первоначальный список товаров.
  • Поисковый запрос, введённый пользователем.
  • Значение чекбокса.
  • Отфильтрованный список товаров.

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

  1. Передаётся ли она от родителя через пропсы? Тогда, наверное, это не состояние.
  2. Остаётся ли она неизменной со временем? Тогда, наверное, это не состояние.
  3. Можете ли вы вычислить её на основании любой другой части состояния или пропсов в своём компоненте? Тогда, наверное, это не состояние.

Исходный список товаров передаётся через пропсы, так что состоянием он быть не может. Поисковый запрос и чекбокс меняются со временем, и их нельзя вычислить из других данных, так что они вполне сойдут за состояние. Напоследок, отфильтрованный список товаров не является состоянием, так как его можно вычислить из оригинального списка, поискового запроса и значения чекбокса.

В итоге, состоянием являются:

  • Поисковый запрос, введённый пользователем
  • Значение чекбокса

Шаг 4: Определим, где должно находиться наше состояние

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

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

Для каждой части состояния в приложении:

  • Определите компоненты, которые рендерят что-то исходя из состояния.
  • Найдите общий главенствующий компонент (компонент, расположенный над другими компонентами, которым нужно это состояние).
  • Либо общий главенствующий компонент, либо любой компонент, стоящий выше по иерархии, должен содержать состояние.
  • Если вам не удаётся найти подходящий компонент, то создайте новый исключительно для хранения состояния и разместите его выше в иерархии над общим главенствующим компонентом.

Давайте применим эту стратегию на примере нашего приложения:

  • Задача ProductTable — отфильтровать список товаров, основываясь на состоянии, а SearchBar — отобразить состояние для поискового запроса и чекбокса.
  • Общий главенствующий компонент для обоих — FilterableProductTable.
  • По идее, имеет смысл содержать текст фильтра и значение чекбокса в FilterableProductTable.

Итак, мы приняли решение расположить наше состояние в FilterableProductTable. Первое, что нужно сделать — добавить свойство this.state = {filterText: '', inStockOnly: false} в конструктор FilterableProductTable, чтобы отобразить начальное состояние нашего приложения. После этого передайте filterText и inStockOnly в ProductTable и SearchBar через пропсы. Напоследок, используйте пропсы для фильтрации строк в ProductTable и определения значений полей формы SearchBar.

Вы заметите изменения в поведении вашего приложения: задайте значение "ball" для filterText и обновите страницу. Вы увидите соответствующие изменения в таблице данных.

Шаг 5: Добавим обратный поток данных

Пока что наше приложение рендерится в зависимости от пропсов и состояния, передающихся вниз по иерархии. Теперь мы обеспечим поток данных в обратную сторону: наша задача сделать так, чтобы компоненты формы в самом низу иерархии обновляли состояние в FilterableProductTable.

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

Если вы попытаетесь ввести текст в поле поиска или установить флажок в чекбоксе данной версии примера, то увидите, что React игнорирует любой ввод. Это преднамеренно, так как ранее мы приравняли значение пропа value в input к state в FilterableProductTable.

Давайте подумаем, как мы хотим изменить поведение. Нам нужно, чтобы при изменениях поисковой формы менялось состояние ввода. Так как компоненты должны обновлять только относящееся к ним состояние, FilterableProductTable передаст колбэк в SearchBar. В свою очередь, SearchBar будет вызывать этот колбэк каждый раз, когда надо обновить состояние. Чтобы получать уведомления об изменениях элементов формы, мы можем использовать событие onChange. Колбэки, переданные из компонента FilterableProductTable, вызовут setState(), и приложение обновится.

Хоть и звучит сложно, но занимает это всего несколько строк кода. А главное, поток данных через приложение остаётся прямым и понятным.

Вот и всё

Надеемся, что этот пример поможет вам получить лучшее представление о том, как подойти к созданию компонентов и приложений в React. Хотя этот процесс и использует немного больше кода, помните: код читают чаще, чем пишут. А модульный и прямолинейный код читается значительно легче. Когда вы начнёте создавать большие библиотеки компонентов, вы сможете по-настоящему оценить прямолинейность и связанность React, а повторно используемые компоненты сделают ваш код намного меньше. :)

Комментарии