Філософія React

З нашої точки зору, React — це відмінний спосіб писати великі і швидкі JavaScript-додатки. Він дуже добре масштабувався для нас у Facebook та Instagram.

Одна з особливостей React – це те, як він змушує думати про додатки в процесі їх створення. У цьому документі ми покажемо хід думок для створення таблиці продуктів з пошуком за допомогою React.

Почнемо з макета

Уявіть, що у нас вже є JSON API і макет дизайну сайту. Він виглядає так:

Макет

Наш JSON API повертає дані, які виглядають наступним чином:

[
  {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. Тому добре побудована модель, як правило, вже відображає призначений для користувача інтерфейс (а значить, і структуру компонентів). Інтерфейс і моделі даних часто мають схожу інформаційну архітектуру, тому розділити інтерфейс на частини не складає труднощів. Розбийте інтерфейс користувача на компоненти, кожен з яких відображає частину моделі даних.

Діаграма компонентів

Ви побачите, що ми маємо п’ять компонентів у нашому додатку. Дані, які представляє кожен компонент, виділено курсивом.

  1. FilterableProductTable (помаранчевий): містить весь приклад
  2. SearchBar (синій): приймає всі вхідні дані користувача
  3. ProductTable (зелений): відображає та фільтрує збір даних на основі вхідних даних користувача
  4. ProductCategoryRow (бірюзовий): відображає заголовок для кожної категорії
  5. ProductRow (червоний): відображає рядок для кожного продукту

Зверніть увагу, що заголовок таблиці всередині ProductTable не є окремим компонентом. Відокремлювати його чи ні — це питання особистих уподобань. У цьому прикладі ми залишили його як частину ProductTable, оскільки він є малою частиною загального збору даних. Проте, якщо в майбутньому заголовок поповниться новими функціями (наприклад, можливістю сортувати товар), є сенс витягти його в самостійний компонент ProductTableHeader.

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

  • FilterableProductTable

    • SearchBar
    • ProductTable

      • ProductCategoryRow
      • ProductRow

Крок 2: Побудуємо статичну версію в React

Приклад коду Філософія React: Крок 2 на CodePen.

Тепер, коли всі компоненти розташовані в ієрархічному порядку, прийшов час реалізувати наш додаток. Найлегший спосіб – створити версію, яка використовує модель даних і рендерить інтерфейс, але не передбачає ніякої інтерактивності. Корисно розділяти ці процеси, тому що написання статичної версії вимагає багато друкування і зовсім небагато мислення. З іншого боку, створення інтерактивності в додатку передбачає більш глибокий розумовий процес і лише частку рутинного друку. Ми розберемося, чому так виходить.

Щоб побудувати статичну версію додатка, яка буде показувати модель даних, нам потрібно створити компоненти, які використовують інші компоненти і передають дані через пропси. Пропси — це спосіб передачи даних від батьків до дочірних елементів. Якщо ви знайомі з поняттям стану, то для статичної версії це якраз те, що вам використовувати не потрібно. Стан передбачає собою дані, які змінюються з часом – інтерактивність. Так як ми працюємо над статичною версією додатка, то нам це не потрібно.

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

Наприкінці цього кроку ви матимете бібліотеку компонентів, які можуть бути використані повторно. Так як це статична версія, то компоненти матимуть тільки методи render(). Компонент вище за ієрархією (FilterableProductTable) буде передавати модель даних через пропси. Якщо ви внесете зміни в базову модель даних і знову викличите ReactDOM.render(), то побачите зміни в інтерфейсі. Немає нічого складного у відостеженні змін та оновленні інтерфейсу. Завдяки односторонньому потоку даних (або односторонній прив’язці) код працює швидко, але залишається зрозумілим.

Якщо у вас залишилися запитання щодо виконання цього кроку, зверніться до документації React.

Невеликий відступ: як пропси відрізняються від стану

Існує два типи “моделі” даних у React: пропси та стан. Важливо, щоб ви розуміли різницю між ними, в іншому випадку зверніться до офіційної документації React.

Крок 3: Визначимо мінімальне (але повноцінне) відображення стану інтерфейсу

Щоб зробити інтерфейс користувача інтерактивним, потрібно, щоб модель даних могла змінюватися з часом. У React це можливо за допомогою стану.

Щоб правильно побудувати додаток, спочатку потрібно подумати про мінімальний набір змінних станів, які потрібні вашому додатку. Головне тут дотримуватися принципу розробки DRY: Don’t Repeat Yourself (укр. не повторюй себе). Визначте мінімальну кількість необхідного стану, який потрібен вашому додатку, все інше обчислюйте за необхідності. Наприклад, якщо ви створюєте список справ, тримайте масив пунктів списку під рукою – але не варто зберігати окремий стан для кількості справ у списку. Якщо треба відобразити кількість елементів, просто використовуйте довжину існуючого масиву.

Давайте перелічимо всі дані у нашому додатку. Ми маємо:

  • Початковий список товарів
  • Пошуковий запит, введений користувачем
  • Значення прапорця
  • Відфільтрований список товарів

Давайте розглянемо кожну частину даних і визначимо, яка з них є станом. Задайте собі наступні три питання:

  1. Передається вона від батька через пропси? Якщо так, тоді, напевно, це не стан.
  2. Залишається вона незмінною з часом? Якщо так, тоді, напевно, це не стан.
  3. Чи можете ви обчислити її на основі будь-якої іншої частини стану або пропсів у своєму компоненті? Якщо так, тоді, напевно, це не стан.

Початковий список товарів передається через пропси, так що станом він бути не може. Пошуковий запит і прапорець змінюються з часом, також їх не можна обчислити з інших даних, тому вони цілком можуть бути станом. Наостанок, відфільтрований список товарів не є станом, так як його можна обчислити з оригінального списку, пошукового запиту та значення прапорця.

У підсумку, наш стан виглядатиме наступним чином:

  • Пошуковий запит
  • Значення прапорця

Крок 4: Визначимо, де має перебувати наш стан

Приклад коду Філософія React: Крок 4 на CodePen.

Отже, ми визначили мінімальний набір станів додатку. Далі нам потрібно з’ясувати, який з компонентів володіє станом або змінює його.

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

Для кожної частини стану в додатку:

  • Визначте компоненти, які рендерять щось на основі цього стану.
  • Знайдіть спільний батьківський компонент (компонент, розташований над іншими компонентами, яким потрібен цей стан).
  • Або спільний батьківський компонент, або будь-який компонент, що стоїть вище за ієрархією, повинен містити стан.
  • Якщо вам не вдається знайти відповідний компонент, створіть один виключно для стану та розмістить його вище за ієрархією над загальним спільним батьківським компонентом.

Давайте застосуємо цю стратегію на прикладі нашого додатку:

  • Завдання ProductTable – відфільтрувати список товарів, базуючись на стані, а завдання SearchBar – відобразити стан для пошукового запиту та прапорця.
  • Спільний батьківський компонент для обох – FilterableProductTable.
  • Згідно ідеї, є сенс помістити текст фільтра та значення прапорця в FilterableProductTable

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

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

Крок 5: Додамо зворотний потік даних

Приклад коду Філософія React: Крок 5 на CodePen.

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

Потік даних у React - односторонній. Це допомагає зрозуміти, як працює додаток, але нам потрібно трохи більше коду, ніж в традиційній двосторонній прив’язці даних.

Якщо ви спробуєте ввести текст у поле пошуку або встановити прапорець в даній версії прикладу, то побачите, що React ігнорує будь-яке введення. Це навмисне, так як раніше ми прирівняли значення пропа value в input до state в FilterableProductTable.

Давайте поміркуємо, як ми хочемо змінити поведінку. Нам потрібно, щоб при змінах пошукової форми змінювався стан введення. Так як компоненти повинні оновлювати тільки той стан, що належить їм, FilterableProductTable передасть функцію зворотнього виклику у SearchBar. У свою чергу, SearchBar викликатиме цю функцію зворотнього виклику кожен раз, коли треба оновити стан. Щоб отримувати повідомлення про зміни елементів форми, ми можемо використовувати подію onChange. Функції зворотнього виклику, передані з FilterableProductTable викличуть setState(), і додаток оновиться.

Бодай звучить складно, але це займає всього кілька рядків коду. А головне, потік даних через додаток залишається прямим і зрозумілим.

От і все

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