Покращуємо компоненти багаторазового використання в React з шаблоном Overrides

10 хв. читання

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

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

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

  • Стилі: Розробникам необхідно мати змогу змінити стилі компонентів та їх внутрішніх частин. Це тривіально для глобального CSS, однак у світі CSS-in-JS, де стилі інкапсульовані у компоненті, а елементи мають довільні класи, усе складніше.
  • Props: Іноді потрібно просто змінити props, передані у внутрішні компоненти. Наприклад, додати елементу aria-label, або передати className, що виступатиме як орієнтир у ваших інтеграційних тестах.
  • Рендеринг: Часто необхідно повністю перевизначити рендеринг чи поведінку певних внутрішніх компонентів. Наприклад, додати в datepicker опцію «швидкого вибору», на зразок «останні 30 днів».

Звичайно, ми не перші, хто намагається розв'язати ці проблеми. Популярний у спільноті React шаблон Render Props надає більше контролю над відображенням компонентів. downshift від Paypal — чудовий приклад як можна використовувати render props.

Для багатьох ситуацій render props — гарне рішення, але якщо вам треба лише змінити стилі чи prop якогось внутрішнього елементу, таке рішення буде «заважким». Автори компонентів іноді пропонують props, на зразок getFooStyle або getFooProps, щоб налаштувати певний внутрішній елемент, але таке рідко трапляється для всіх внутрішніх елементів.

Хотілося б запропонувати єдиний API для всіх наших компонентів, який забезпечуватимe повну гнучкість рендерингу, але також матимe прості рішення для поширeних випадків, коли треба лише змінити деякий внутрішній стиль чи prop.

Так з'явився шаблон «Overrides».

Публічне API Overrides

Фрагмент коду нижче демонструє як виглядає шаблон overrides при налаштуванні багаторазового об'єкта Autocomplete.

// App.js
render() {
  <Autocomplete
    options={this.props.products} 
    overrides={{
      Root: {
        props: {'aria-label': 'Select an option'},
        style: ({$isOpen}) => ({borderColor: $isOpen ? 'blue' : 'grey'}),
      },
      Option: {
        component: CustomOption
      }
    }}
  />
}

// CustomOption.js
export default function CustomOption({onClick, $option}) {
  return (
    <div onClick={onClick}>
      <h4>{$option.name}</h4>
      <small>{$option.price}</small>
    </div>
  );
}

Тут багато що відбувається, тому розглянемо деякі ключові зміни:

  • Кожному внутрішньому елементу присвоюється ідентифікатор, на який можуть орієнтуватися розробники. Тут ми використовуємо Root та Option. Можна вважати їх майже назвами класів (але без недоліків каскаду СSS та глобального простору імен).
  • Для кожного внутрішнього елементу, ви можете перевизначити props, стилі та компонент.
  • Перевизначення props досить просте. Об'єкт, який ви визначаєте буде поширюватись з props за замовчуванням, отримуючи перевагу над ними. Тут ми використовуємо props, щоб додати aria-label до кореневого елементу.
  • При перевизначенні стилів, ви можете передати як об'єкт стилю, так і функцію, що приймає деякі props стосовно поточного внутрішнього стану компонента. Це дає змогу динамічно змінювати стилі на основі стану компонента (наприклад, isError або isSelected). Повернений функцією об'єкт стилю об'єднаний зі стилями елементів за замовчуванням.
  • При перевизначенні компонента, ви можете передати функціональний компонент без стану або клас компонента React, в якому задається власна логіка рендерингу і, при необхідності, додаються інші обробники або поведінка. По суті це впровадження залежностей, якe відкриває потужні можливості.

Уся функціональність забезпечується єдиним overrides prop, що дає розробникам відповідне місце для необхідних налаштувань.

Overrides у дії

Щоб ви мали уявлення як усе працює, розглянемо приклад від команди Uber Freight:

Покращуємо компоненти багаторазового використання в React з шаблоном Overrides
Перевизначення компонента RadioGroup для Uber Freight

Вони хотіли створити елемент форми з тим самим API, контролем клавіатури, що поводитeмиться як група радіо-кнопок, але матимe інший вигляд. Для цього додали послідовність перевизначень (overrides) стилю поверх наявного компонента RadioGroup замість того, щоб марно створювати, тестувати та підтримувати власну реалізацію.

Покращуємо компоненти багаторазового використання в React з шаблоном Overrides
Додаємо поведінку «Edit» до тегу з перевизначенням компонента

Наступним прикладом, яким ми керувались при створенні шаблону було додавання поведінки кнопки «Редагувати» до тегу у компоненті multi-select. Тут ми перевизначили компонент Tag, що рендерив наявний контент, але до того ж мав значок редагування.

Бачимо одну з переваг введення повних компонентів: на відміну від render props, ви можете створити новий стан, методи життєвого циклу або навіть react хуки, якщо потрібно. Наш компонент EditableTag міг за кліком показувати модальне вікно, а потім запускати необхідні дії redux для оновлення вмісту тегу.

Overrides для внутрішньої реалізації

Так ми можемо реалізувати overrides внутрішньо для нашого компоненту Autocomplete :

// Autocomplete.js
import React from 'react';
import * as defaultComponents from './styled-elements';

class Autocomplete extends React.Component {

  // налаштування та обробники пропущено для скорочення
  
  getSharedStyleProps() {
    const {isOpen, isError} = this.state;
    return {
      $isOpen: isOpen
      $isError: isError
    };
  }

  render() {
    const {isOpen, query, value} = this.state;
    const {options, overrides} = this.props;

    const {
      Root: {component: Root, props: rootProps},
      Input: {component: Input, props: inputProps},
      List: {component: List, props: listProps},
      Option: {component: Option, props: optionProps},
    } = getComponents(defaultComponents, overrides);

    const sharedProps = this.getSharedStyleProps();

    return (
      <Root {...sharedProps} {...rootProps}>
        <Input 
          type="text"
          value={query} 
          onChange={this.onInputChange}
          {...sharedProps}
          {...inputProps}
        />
        {isOpen && (
          <List {...sharedProps} {...listProps}>
            {options.map(option => {
              <Option
                onClick={this.onOptionClick.bind(this, option)}
                $option={option}
                {...sharedProps}
                {...optionProps}
              >
                {option.label}
              </Option>
            })}
          </List>
        )}
      </Root>
    );
  }
}

Зверніть увагу, що метод render не має примітивів DOM, на зразок <div>. Натомість ми імпортуємо набір суб-компонентів за замовчуванням з сусіднього файлу. Там використано бібліотеку CSS-in-JS для створення компонентів, які інкапсулюють усі стилі за замовчуванням. Якщо реалізація компонента передається в overrides, вона має пріоритет над значеннями за замовчуванням.

getComponents — простий хелпер для розпакування overrides та об'єднання з компонентами стилю за замовчуванням. Існує безліч способів реалізувати це, але ось найкоротший варіант:

function getComponents(defaultComponents, overrides) {
  return Object.keys(defaultComponents).reduce((acc, name) => {
    const override = overrides[name] || {};
    acc[name] = {
      component: override.component || defaultComponents[name],
      props: {$style: override.style, ...override.props},
    };
    return acc;
  }, {});
}

Функція з фрагмента завжди присвоює стилі overrides до prop $style та об'єднує їх з будь-якими іншими перевизначеними props. Усе завдяки тому, що наш CSS-in-JS шукає prop $style і об'єднує його зі стилями за замовчуванням.

Кожен суб-компонент також отримує sharedProps — набір props, які стосуються стану компонента і можуть використовуватись для динамічної зміни стилів або рендерингу (наприклад, зміна кольору рамки на червоний, якщо виникла помилка). Назва таких props, згідно з правилом про іменування починається з $, щоб вказати, що це особливі props, які не слід передавати як атрибут основного елементу DOM.

Компроміси та проблеми

Як і з більшістю шаблонів проектування, існує кілька компромісів, які слід враховувати при використанні overrides.

Строгість

Оскільки кожному внутрішньому елементу надається ідентифікатор, який розглядається як орієнтир для overrides, модифікація структури DOM частіше призводитиме до критичних змін. Це стосується і змін CSS: модифікація елемента з display: flex на display: block може, теоретично, призвести до критичної зміни, якщо користувач перевизначає дочірній елемент, а він всередині flexbox. Проблеми, що зазвичай охайно інкапсулюються всередині вашого компонента, можуть потім себе проявити.

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

Документація

Тепер, коли ваші внутрішні елементи — частина відкритого API, ви захочете створити документацію, яка описує кожен елемент і які props він отримує, включаючи просту діаграму структури DOM, де елементи позначені їх ідентифікаторами.

Статичне додавання об'єкта overrides для кожного компонента, використовуючи Typescript або Flow, — також великий крок вперед. Так стане зрозуміліше які props отримає кожен компонент і чи є сумісними overrides, які передаються.

Композиція

Уявіть, що ви створюєте повторно використовуваний компонент Pagination, в якому розташовується Button. Як ми застосуємо overrides до Button через Pagination? А якщо є декілька кнопок, і користувач хоче застосувати до них різні стилі? Ось деякі ідеї щодо розв'язання цієї проблеми, але досі немає певного рішення.

Складність

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

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.6K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація