Автор статьи, перевод которой мы сегодня публикуем, говорит, что в компании commercetools приняли на вооружение хуки React в начале 2019 года — в момент их появления в React 16.8.0. С тех пор программисты компании постоянно перерабатывают свой код, переводя его на хуки. Хуки React позволяют, не используя классы, работать с состоянием компонентов и пользоваться другими возможностями React. Используя хуки, можно, работая с функциональными компонентами, «подключаться» к событиям жизненного цикла компонентов и реагировать на изменения их состояния.
Кратко о результатах внедрения хуков
Если в двух словах рассказать о том, что дали нам хуки, то получится, что они помогли упростить кодовую базу, извлекая логику из компонентов и облегчая композицию разных возможностей. Более того, применение хуков привело к тому, что мы многому научились. Например — тому, как лучше структурировать код через постоянное совершенствование существующего функционала. Мы уверены, что узнаем ещё много интересного, продолжая работать с хуками React.
Сильные стороны внедрения хуков
Вот что дало нам внедрение хуков:
- Улучшение читабельности кода. Это возможно благодаря использованию деревьев компонентов меньшего, чем раньше, размера. Созданию таких деревьев компонентов способствует наше стремление отказа от свойств рендеринга и от компонентов высшего порядка.
- Совершенствование возможностей по отладке кода. В нашем распоряжении оказалось улучшенное визуальное представление кода и дополнительные отладочные сведения, предоставляемые инструментами разработчика React.
- Повышение уровня модульности проекта. За счёт функциональной природы хуков упростился процесс создания и применения логики, подходящей для многократного использования.
- Разделение ответственностей. Компоненты React отвечают за внешний вид элементов интерфейса, а хуки дают доступ к инкапсулированной логике программ.
Теперь мы хотим поделиться нашими рекомендациями по использованию хуков в продакшне.
1. Своевременное извлечение хуков из компонентов и создание пользовательских хуков
Начать пользоваться хуками в функциональных компонентах довольно легко. Мы, например, по-быстрому применяли где-то React.useState
, где-то — React.useEffect
, и продолжали делать своё дело. Такой подход, правда, не позволяет в полной мере воспользоваться всеми теми возможностями, которые способны дать хуки. Представляя React.useState
в виде маленького use<StateName>State
, а React.useEffect
— в виде use<EffectName>Effect
, мы смогли добиться следующего:
- Компоненты оказываются меньше, чем при обычном использовании хуков.
- Хуки можно многократно использовать в различных компонентах.
- Мы, в ходе отладки кода с помощью инструментов разработчика React, получаем имена, лучше описывающие программные механизмы. Например, имя вида
State<StateName>
вместо простогоState
. Если в одном компоненте несколько раз используетсяReact.useState
, такой подход оказывается особенно полезным.
Извлечение хуков способствует ещё и тому, что многократно используемая и совместно используемая логика оказывается более заметной в различных частях приложения. Похожую или дублирующуюся логику сложнее заметить в том случае, если пользуются лишь встроенными в код компонентов хуками. Получаемые хуки могут иметь маленький размер и содержать совсем немного кода — наподобие useToggleState
. С другой стороны, более масштабные хуки, вроде useProductFetcher
, теперь могут включать в себя больше функционала. И то и другое помогло нам упростить кодовую базу за счёт уменьшения размеров React-компонентов.
В следующем примере показано создание маленького React-хука, предназначенного для управления выбором элементов. Преимущества инкапсуляции этого функционала становятся очевидными сразу после того, как осознаёшь то, как часто подобная логика используется в приложении. Например — для выбора набора заказов из списка.
// Вместо этого
function Component() {
const [selected, setSelected] = React.useState()
const select = React.useCallback(id => setSelected(/** ... */), [
selected,
setSelect
])
return <List selectedIds={selected} onSelect={id => select(id)} />
}
// Можно сделать следующее
const useSelection = () => {
const [selected, setSelected] = React.useState()
const select = React.useCallback(id => setSelected(/** ... */), [
selected,
setSelect
])
return [selected, select]
}
function Component() {
const [selected, select] = useSelection()
return <List selectedIds={selected} onSelect={id => select(id)} />
}
2. О пользе React.useDebugValue
Стандартный хук React.useDebugValue относится к малоизвестным возможностям React. Этот хук может помочь разработчику в ходе отладки кода, он способен оказаться полезным при его применении в хуках, рассчитанных на совместное использование. Это — пользовательские хуки, которые применяются во многих компонентах приложения. Однако useDebugValue
не рекомендуется применять во всех пользовательских хуках, так как встроенные хуки уже логируют стандартные отладочные значения.
Представьте себе создание пользовательского хука, предназначенного для принятия решения о необходимости включения некой возможности приложения. Данные состояния приложения, на которых основывается принятие решения, могут поступать из разных источников. Они могут быть сохранены в объекте контекста React, доступ к которому организован через React.useContext
.
Для того чтобы помочь разработчику в отладке, в ходе работы с инструментами разработчика React может оказаться полезным знание об анализируемом имени флага (flagName
) и варианте значения флага (flagVariation
). В этом деле нам может помочь использование маленького хука React.useDebugValue
:
export default function useFeatureToggle(flagName, flagVariation = true) {
const flags = React.useContext(FlagsContext)
const isFeatureEnabled = getIsFeatureEnabled(flagName, flagVariation)(flags)
React.useDebugValue({
flagName,
flagVariation,
isEnabled: isFeatureEnabled
})
return isFeatureEnabled
}
Теперь, работая с инструментами разработчика React, мы можем видеть сведения о варианте значения флага, об имени флага, и о том, включена или нет интересующая нас возможность.
Работа с инструментами разработчика React после применения хука React.useDebugValue
Обратите внимание на то, что в тех случаях, когда в пользовательских хуках используются стандартные хуки вроде React.useState
или React.useRef
, такие хуки уже логируют соответствующие данные о состоянии или о ref-объектах. В результате использование здесь конструкций вида React.useDebugValue({ state })
оказывается не особенно полезным.
3. Комбинирование и композиция хуков
Когда мы начали внедрять в работу хуки и стали использовать всё больше и больше хуков в компонентах, быстро оказалось, что в одном и том же компоненте могло присутствовать 5-10 хуков. При этом применялись хуки самых разных типов. Например, мы могли использовать 2-3 хука React.useState
, далее — хук React.useContext
(скажем, для получения сведений об активном пользователе), хук React.useEffect
, а также — хуки из других библиотек, вроде react-router или react-intl.
Вышеописанное повторялось снова и снова, в результате даже очень маленькие компоненты оказывались не такими уж и компактными. Для того чтобы этого избежать, мы начали извлекать эти отдельные хуки в пользовательские хуки, устройство которых зависело от компонента или некоей возможности приложения.
Представьте себе механизм приложения, направленный на создание заказа. При разработке этого механизма использовалось множество компонентов, а также — хуки различных типов. Эти хуки можно скомбинировать в виде пользовательского хука, повысив удобство их применения в компонентах. Вот пример комбинирования набора маленьких хуков в одном хуке.
function OrderCreate() {
const {
orderCreater,
orderFetcher,
applicationContext,
goToNextStep,
pendingOrder,
updatePendingChanges,
revertPendingChanges
} = useOrderCreate()
return (/** ...children */)
}
4. Сравнение React.useReducer и React.useState
Мы часто прибегали к React.useState
как к стандартному хуку для работы с состоянием компонентов. Однако со временем состояние компонента может нуждаться в усложнении, что зависит от новых требований к компоненту, наподобие наличия в состоянии нескольких значений. В определённых случаях использование React.useReducer
может помочь избежать необходимости использования нескольких значений и упростить логику обновления состояния. Представьте себе управление состоянием при выполнении HTTP-запросов и получении ответов. Для этого может понадобиться работать с такими значениями, как isLoading
, data
и error
. Вместо этого состоянием можно управлять с помощью редьюсера, имея в своём распоряжении различные действия для управления изменениями состояния. Такой подход, в идеале, подталкивает разработчика к тому, чтобы он воспринимал состояния интерфейса в виде машины состояний.
Редьюсер, передаваемый React.useReducer
, похож на редьюсеры, применяемые в Redux, где система получает текущее состояние действия и должна вернуть следующее состояние. Действие содержит сведения о своём типе, а также данные, на основе которых формируется следующее состояние. Вот пример простейшего редьюсера, предназначенного для управления неким счётчиком:
const initialState = 0;
const reducer = (state, action) => {
switch (action) {
case 'increment': return state + 1;
case 'decrement': return state - 1;
case 'reset': return 0;
default: throw new Error('Unexpected action');
}
};
Этот редьюсер может быть протестирован в изоляции, а затем использован в компоненте с применением React.useReducer
:
function Component() {
const [count, dispatch] = React.useReducer(reducer, initialState);
return (
<div>
{count}
<button onClick={() => dispatch('increment')}>+1</button>
<button onClick={() => dispatch('decrement')}>-1</button>
<button onClick={() => dispatch('reset')}>reset</button>
</div>
);
};
Здесь мы можем применить то, что разобрали в предыдущих трёх разделах, а именно — можем извлечь всё в useCounterReducer
. Это улучшит код, скрыв сведения о типах действия от компонента, описывающего внешний вид элемента интерфейса. В результате это поможет предотвратить утечку деталей реализации в компонент, а также даст нам дополнительные возможности по отладке кода. Вот как будут выглядеть полученный в результате пользовательский хук и компонент, использующий его:
const CounterActionTypes = {
Increment: 'increment',
Decrement: 'decrement',
Reset: 'reset',
}
const useCounterReducer = (initialState) => {
const [count, dispatch] = React.useReducer(reducer, initialState);
const increment = React.useCallback(() => dispatch(CounterActionTypes.Increment));
const decrement = React.useCallback(() => dispatch(CounterActionTypes.Decrement));
const reset = React.useCallback(() => dispatch(CounterActionTypes.Reset));
return {
count,
increment,
decrement
}
}
function Component() {
const {count, increment} = useCounterReducer(0);
return (
<div>
{count}
<button onClick={increment}>+1</button>
</div>
);
};
5. Постепенное внедрение хуков
На первый взгляд идея постепенного внедрения хуков может показаться не вполне логичной, но тут я предлагаю тем, кто так считает, просто следовать за моими рассуждениями. С течением времени в кодовой базе находят применение различные паттерны. В нашем случае эти паттерны включают в себя компоненты высшего порядка, свойства рендеринга, а теперь — и хуки. При переводе проекта на новый паттерн разработчики не стремятся мгновенно переписать весь код, что, в общем-то, практически невозможно. В результате нужно выработать план перевода проекта на хуки React, который не предусматривает внесение в код больших изменений. Эта задача может оказаться очень непростой, так как внесение в код изменений обычно ведёт к увеличению его размеров и сложности. Мы, внедряя хуки, стремимся к тому, чтобы этого избежать.
В нашей кодовой базе используются компоненты, основанные на классах, и функциональные компоненты. Вне зависимости от того, какой именно компонент был применён в некоей ситуации, мы стремились к совместному использованию логики посредством хуков React. Сначала мы реализуем логику в хуках (или повторяем в них уже существующую реализацию неких механизмов), затем создаём маленькие компоненты высшего порядка, внутри которых используются эти хуки. После этого данные компоненты высшего порядка применяются при создании компонентов, основанных на классах. В результате в нашем распоряжении оказывается логика, расположенная в одном месте, которая может быть использована в компонентах разных видов. Вот пример внедрения функциональности хуков в компоненты через компоненты высшего порядка.
export const injectTracking = (propName = 'tracking') => WrappedComponent => {
const WithTracking = props => {
const tracking = useTracking();
const trackingProp = {
[propName]: tracking,
};
return <WrappedComponent {...props} {...trackingProp} />;
};
WithTracking.displayName = wrapDisplayName(WrappedComponent, 'withTracking');
return WithTracking;
};
export default injectTracking;
Здесь показано внедрение функционала хука useTracking
в компонент WrappedComponent
. Это, кстати, позволило нам, кроме прочего, ещё и разделить задачи по внедрению хуков и по переписыванию тестов в старых частях системы. При таком подходе в нашем распоряжении всё ещё остаются механизмы для применения хуков во всех частях кодовой базы.
Итоги
Здесь были показаны некоторые примеры того, как применение хуков React позволило улучшить нашу кодовую базу. Надеемся, хуки смогут принести пользу и вашему проекту.
Уважаемые читатели! Пользуетесь ли вы хуками React?
Автор: ru_vds