⚛ 5 продвинутых паттернов React-разработки

Обзор пяти современных передовых шаблонов разработки на React с их достоинствами и недостатками, а также примерами кода.

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

Идеальный библиотечный компонент React:

  • Предоставляет простой и понятный API;
  • Имеет несколько модификаций и вариантов использования, легко настраивается при необходимости;
  • Позволяет расширять и тонко контролировать свое поведение (для сложных ситуаций).

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

Для удобства мы разберем все паттерны на одном примере и по одному плану. В качестве компонента будет выступать простой счетчик:

Компонент Counter будет реализован пятью разными способами

Для каждого шаблона будет небольшое введение, реальный вариант использования (со ссылкой на GitHub, где вы найдете и примеры реализации) и разбор плюсов и минусов. Затем подведем небольшой итог и выставим оценки по двум критериям:

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

Весь исходный код доступен на GitHub: https://github.com/alex83130/advanced-react-patterns.

Также посмотрим, какие публичные библиотеки React уже используют тот или иной паттерн.

Паттерн #1. Составные компоненты

Этот шаблон разработки позволяет создавать понятные декларативные компоненты без многоуровневого пробрасывания пропсов. Его основное достоинство – разделение ответственности между несколькими элементами. Составные компоненты проще настраивать, и API у них максимально простой.

Пример использования

Github: https://github.com/alex83130/advanced-react-patterns/tree/main/src/patterns/compound-component

compound-component.js
import React from "react";
import { Counter } from "./Counter";

function Usage() {
  const handleChangeCounter = (count) => {
    console.log("count", count);
  };

  return (
    <Counter onChange={handleChangeCounter}>
      <Counter.Decrement icon="minus" />
      <Counter.Label>Counter</Counter.Label>
      <Counter.Count max={10} />
      <Counter.Increment icon="plus" />
    </Counter>
  );
}

export { Usage };

Плюсы

Уменьшается сложность API

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

Гибкая структура разметки

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

Разделение ответственности

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

Минусы

Слишком большая гибкость пользовательского интерфейса

Гибкость – это не всегда хорошо. Без должного контроля, она может привести к изменению интерфейса или даже поломке компонента. Например, ничто не мешает пользователю добавить дополнительный элемент или, наоборот, забыть что-то важное (подкомпонент или параметр).

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

Громоздкая разметка

Очевидно, что количество строк разметки существенно увеличивается, ведь каждый элемент представлен отдельным компонентом, а не спрятан внутри родителя. Особенно это чувствуется при использовании линтеров (ESLint) или форматировщиков кода (Prettier).

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

Оценки

  • Инверсия управления: 1 из 4
  • Сложность реализации: 1 из 4

Публичные библиотеки, использующие этот паттерн

Паттерн #2. Управление свойствами

Этот шаблон предназначен для создания управляемых компонентов. При этом внешнее состояние используется как “единственный источник истины”, и пользователь может изменять дефолтное поведение компонента, добавляя собственную логику.

Пример использования

GitHub: https://github.com/alex83130/advanced-react-patterns/tree/main/src/patterns/control-props

control-props.js
import React, { useState } from "react";
import { Counter } from "./Counter";

function Usage() {
  const [count, setCount] = useState(0);

  const handleChangeCounter = (newCount) => {
    setCount(newCount);
  };
  return (
    <Counter value={count} onChange={handleChangeCounter}>
      <Counter.Decrement icon={"minus"} />
      <Counter.Label>Counter</Counter.Label>
      <Counter.Count max={10} />
      <Counter.Increment icon={"plus"} />
    </Counter>
  );
}

export { Usage };

Плюсы

Больше контроля

Пользователь полностью контролирует состояние компонента и напрямую влияет на его поведение.

Минусы

Сложность реализации

Управляемый компонент нельзя просто подключить в одном месте и забыть. Требуется написать больше кода (JSX-разметка, useState и обработчик handleChange)

Оценки

  • Инверсия управления: 2 из 4
  • Сложность реализации: 1 из 4

Публичные библиотеки, использующие этот шаблон

Паттерн #3. Кастомные хуки

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

Пример использования

GitHub: https://github.com/alex83130/advanced-react-patterns/tree/main/src/patterns/custom-hooks

custom-hooks.js
import React from "react";
import { Counter } from "./Counter";
import { useCounter } from "./useCounter";

function Usage() {
  const { count, handleIncrement, handleDecrement } = useCounter(0);
  const MAX_COUNT = 10;

  const handleClickIncrement = () => {
    // ... пользовательская логика
    if (count < MAX_COUNT) {
      handleIncrement();
    }
  };

  return (
    <>
      <Counter value={count}>
        <Counter.Decrement
          icon={"minus"}
          onClick={handleDecrement}
          disabled={count === 0}
        />
        <Counter.Label>Counter</Counter.Label>
        <Counter.Count />
        <Counter.Increment
          icon={"plus"}
          onClick={handleClickIncrement}
          disabled={count === MAX_COUNT}
        />
      </Counter>
      <button onClick={handleClickIncrement} disabled={count === MAX_COUNT}>
        Custom increment btn 1
      </button>
    </>
  );
}

export { Usage };

Плюсы

Больше контроля

Пользователь может добавить свою собственную логику между вашим хуком и элементом JSX, что позволяет изменить дефолтное поведение компонента.

Минусы

Сложность реализации

Логическая часть компонента полностью отделена от рендеринга, а значит пользователю придется связать их самостоятельно. Чтобы реализовать все правильно, разработчик должен хорошо понимать, как работает система в целом.

Оценки

  • Инверсия управления: 2 из 4
  • Сложность реализации: 2 из 4

Публичные библиотеки, использующие этот шаблон

Паттерн #4. Геттер пропсов

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

Замаскировать эту сложность пытается шаблон Props Getter. Компонент предоставляет геттеры, которые возвращают список пропсов для связи с определенным элементом JSX-разметки. Теперь пользователь не должен указывать атрибуты самостоятельно, достаточно просто передать полученный список.

При этом остается возможность переопределить необходимые свойства.

Пример использования

GitHub: https://github.com/alex83130/advanced-react-patterns/tree/main/src/patterns/props-getters

props-getters.js
import React from "react";
import { Counter } from "./Counter";
import { useCounter } from "./useCounter";

const MAX_COUNT = 10;

function Usage() {
  const {
    count,
    getCounterProps,
    getIncrementProps,
    getDecrementProps
  } = useCounter({
    initial: 0,
    max: MAX_COUNT
  });

  const handleBtn1Clicked = () => {
    console.log("btn 1 clicked");
  };

  return (
    <>
      <Counter {...getCounterProps()}>
        <Counter.Decrement icon={"minus"} {...getDecrementProps()} />
        <Counter.Label>Counter</Counter.Label>
        <Counter.Count />
        <Counter.Increment icon={"plus"} {...getIncrementProps()} />
      </Counter>
      <button {...getIncrementProps({ onClick: handleBtn1Clicked })}>
        Custom increment btn 1
      </button>
      <button {...getIncrementProps({ disabled: count > MAX_COUNT - 2 })}>
        Custom increment btn 2
      </button>
    </>
  );
}

export { Usage };

Плюсы

Простота использования

Интеграция компонента в код становится проще, пользователю нужно только подключить правильный геттер к правильному элементу JSX. Дополнительная сложность от него скрыта.

Гибкость

Пользователь компонента по-прежнему имеет возможность перегружать пропсы при необходимости.

Минусы

Непрозрачность

Геттеры привносят дополнительный уровень абстракции, что облегчает интеграцию компонента, но одновременно и делает его менее прозрачным, "магическим". При этом чтобы правильно переопределить какое-либо свойство, очень важно понимать их внутреннюю логику и ничего не сломать.

Оценки

  • Инверсия управления: 3 из 4
  • Сложность интеграции: 3 из 4

Публичные библиотеки, использующие этот шаблон

Паттерн #5. Редуктор состояния

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

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

Пример использования

GitHub: https://github.com/alex83130/advanced-react-patterns/tree/main/src/patterns/state-reducer

state-reducer.js
import React from "react";
import { Counter } from "./Counter";
import { useCounter } from "./useCounter";

const MAX_COUNT = 10;
function Usage() {
  const reducer = (state, action) => {
    switch (action.type) {
      case "decrement":
        return {
          count: Math.max(0, state.count - 2) //The decrement delta was changed for 2 (Default is 1)
        };
      default:
        return useCounter.reducer(state, action);
    }
  };

  const { count, handleDecrement, handleIncrement } = useCounter(
    { initial: 0, max: 10 },
    reducer
  );

  return (
    <>
      <Counter value={count}>
        <Counter.Decrement icon={"minus"} onClick={handleDecrement} />
        <Counter.Label>Counter</Counter.Label>
        <Counter.Count />
        <Counter.Increment icon={"plus"} onClick={handleIncrement} />
      </Counter>
      <button onClick={handleIncrement} disabled={count === MAX_COUNT}>
        Custom increment btn 1
      </button>
    </>
  );
}

export { Usage };

В этом примере объединены паттерны редуктора состояния и кастомного хука. Но ничего не мешает вам использовать редуктор с составными компонентами, передав его главному компоненту Counter.

Плюсы

Больше контроля

Использование редукторов состояний – лучший способ передать управление компонентом пользователю. Оно идеально подходит для сложных случаев, когда требуется самая тонкая настройка. Все действия компонента доступны извне и могут быть переопределены.

Минусы

Сложность реализации

Этот паттерн, безусловно, является самым сложным для реализации, как для вас, так и для пользователя.

Непрозрачность

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

Оценки

  • Инверсия управления: 4 из 4
  • Сложность интеграции: 4 из 4

Публичные библиотеки, использующие этот шаблон

***
Мы разобрали пять продвинутых React-паттернов, использующих концепцию инверсии управления. Они позволяют создавать компоненты с необходимым уровнем гибкости и адаптируемости.

Нельзя забывать о том, что "с большой силой приходит большая ответственность, Питер". Чем больший контроль вы передаете пользователю, тем сложнее работать с вашим компонентом – его уже не получится просто подключить и сразу же начать пользоваться. Вы, как разработчик, должны самостоятельно определить, какой шаблон отвечает вашим задачам больше всего.

Вам в помощь небольшая диаграмма, классифицирующая все рассмотренные паттерны по сложности интеграции и инверсии управления.

Источники

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