Обзор пяти современных передовых шаблонов разработки на 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 Публичные библиотеки, использующие этот паттерн React Bootstrap Reach UI Паттерн #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 Публичные библиотеки, использующие этот шаблон Material UI Паттерн #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 Публичные библиотеки, использующие этот шаблон React table React hook form Паттерн #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 Публичные библиотеки, использующие этот шаблон React table Downshift Паттерн #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 Публичные библиотеки, использующие этот шаблон Downshift *** Мы разобрали пять продвинутых React-паттернов, использующих концепцию инверсии управления. Они позволяют создавать компоненты с необходимым уровнем гибкости и адаптируемости. Нельзя забывать о том, что «с большой силой приходит большая ответственность, Питер». Чем больший контроль вы передаете пользователю, тем сложнее работать с вашим компонентом – его уже не получится просто подключить и сразу же начать пользоваться. Вы, как разработчик, должны самостоятельно определить, какой шаблон отвечает вашим задачам больше всего. Вам в помощь небольшая диаграмма, классифицирующая все рассмотренные паттерны по сложности интеграции и инверсии управления.
Каждый хороший разработчик должен думать о качестве кода и удобстве его использования. Особенно это важно, если вашим кодом будут пользоваться другие разработчики, например, если вы пишете библиотеку компонентов. В этом случае особенно важным становится вопрос контроля и расширяемости.
Идеальный библиотечный компонент React:
В поисках этого Идеального компонента сообщество React разработало несколько классных паттернов, которые вы должны взять на вооружение. Все они так или иначе позволяют разработчику вмешиваться в работу компонента и настраивать или модифицировать его под свои нужды.
Для удобства мы разберем все паттерны на одном примере и по одному плану. В качестве компонента будет выступать простой счетчик:
Компонент Counter будет реализован пятью разными способами
Для каждого шаблона будет небольшое введение, реальный вариант использования (со ссылкой на GitHub, где вы найдете и примеры реализации) и разбор плюсов и минусов. Затем подведем небольшой итог и выставим оценки по двум критериям:
Весь исходный код доступен на GitHub: https://github.com/alex83130/advanced-react-patterns.
Также посмотрим, какие публичные библиотеки React уже используют тот или иной паттерн.
Этот шаблон разработки позволяет создавать понятные декларативные компоненты без многоуровневого пробрасывания пропсов. Его основное достоинство – разделение ответственности между несколькими элементами. Составные компоненты проще настраивать, и 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).
В масштабе одного компонента это не кажется большой проблемой, но если вы посмотрите на общую картину, то, возможно, передумаете.
Этот шаблон предназначен для создания управляемых компонентов. При этом внешнее состояние используется как “единственный источник истины”, и пользователь может изменять дефолтное поведение компонента, добавляя собственную логику.
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)
Мы можем пойти еще дальше в инверсии управления и перенести основную логику компонента в кастомный хук, который доступен пользователю и предоставляет несколько внутренних логик (состояния, обработчики). Таким образом, пользователь может лучше контролировать ваш компонент.
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, что позволяет изменить дефолтное поведение компонента.
Логическая часть компонента полностью отделена от рендеринга, а значит пользователю придется связать их самостоятельно. Чтобы реализовать все правильно, разработчик должен хорошо понимать, как работает система в целом.
Паттерн с использованием кастомного хука дает большой контроль, но также затрудняет интеграцию вашего компонента, потому что пользователю приходится иметь дело с большим количеством пропсов и воссоздавать логику на своей стороне.
Замаскировать эту сложность пытается шаблон 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. Дополнительная сложность от него скрыта.
Гибкость
Пользователь компонента по-прежнему имеет возможность перегружать пропсы при необходимости.
Непрозрачность
Геттеры привносят дополнительный уровень абстракции, что облегчает интеграцию компонента, но одновременно и делает его менее прозрачным, «магическим». При этом чтобы правильно переопределить какое-либо свойство, очень важно понимать их внутреннюю логику и ничего не сломать.
Самый продвинутый с точки зрения инверсии контроля паттерн, который дает пользователю расширенные возможности изменить внутреннюю работу вашего компонента.
Код похож на паттерн с кастомным хуком, но здесь пользователь еще должен сам написать редуктор, который будет обрабатывать все внутренние действия компонента, и передать его хуку.
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.
Использование редукторов состояний – лучший способ передать управление компонентом пользователю. Оно идеально подходит для сложных случаев, когда требуется самая тонкая настройка. Все действия компонента доступны извне и могут быть переопределены.
Этот паттерн, безусловно, является самым сложным для реализации, как для вас, так и для пользователя.
Так как любое действие редуктора может быть изменено, от пользователя требуется хорошее понимание внутренней логики компонента.
*** Мы разобрали пять продвинутых React-паттернов, использующих концепцию инверсии управления. Они позволяют создавать компоненты с необходимым уровнем гибкости и адаптируемости.
Нельзя забывать о том, что «с большой силой приходит большая ответственность, Питер». Чем больший контроль вы передаете пользователю, тем сложнее работать с вашим компонентом – его уже не получится просто подключить и сразу же начать пользоваться. Вы, как разработчик, должны самостоятельно определить, какой шаблон отвечает вашим задачам больше всего.
Вам в помощь небольшая диаграмма, классифицирующая все рассмотренные паттерны по сложности интеграции и инверсии управления.
Ваш адрес email не будет опубликован. Обязательные поля помечены *
Сохранить моё имя, email и адрес сайта в этом браузере для последующих моих комментариев.
Δ
Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.