React hooks — победа или поражение?
С выходом нового React 16.6.0 в документации появился HOOKS (PROPOSAL). Они сейчас доступны в react 17.0.0-alpha и обсуждаются в открытом RFC: React Hooks. Давайте разберемся что это такое и зачем это нужно под катом.
Да это RFC и вы можете повлиять на конечную реализацию обсуждая с создателями react почему они выбрали тот или иной подход.
Давайте взглянем на то как выглядит стандартный хук:
import { useState } from 'react';
function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
return (
You clicked {count} times
);
}
Попробуйте обдумать этот код, это тизер и к концу статьи вы уже будете понимать, что он означает. Первое, что стоит знать, что это не ломает обратную совместимость и возможно их добавят в 16.7 после сбора обратной связи и пожеланий в RFC.
Как уверяют ребята, это не план по выпиливанию классов из реакта.
Так же хуки не заменяют текущие концепции реакта, все на месте props/state/context/refs. Это всего лишь еще один способ использовать их силу.
Мотивация
Хуки решают на первый взгляд не связные проблемы, которые появились при поддержке десятков тысяч компонентов в течении 5 лет у facebook.
Самое сложное это переиспользовать логику в stateful компонентах, у реакта нет способа прикрепить многоразовое поведение к компоненту (например подключить его к хранилищу). Если вы работали с React вам известно понятие HOC (high-order-component) или render props. Это достаточно хорошие паттерны, но иногда они используются чрезмерно, они требуют реструктуризации компонентов, для того, чтобы их можно было использовать, что обычно делает код более громоздким. Стоит посмотреть на типичное реакт приложение и станет понятно о чем идет речь.
Это называется wrapped-hell — ад оберток.
Приложение из одних HOC это нормально в текущих реалиях, подключили компонент к стору/теме/локализации/кастомным хокам, я думаю это всем знакомо.
Становится понятно, что реакту необходим другой примитивный механизм для разделения логики.
С помощью хуков мы можем извлекать состояние компонента, так чтобы его можно было тестировать и переиспользовать. Хуки позволяют повторно использовать логику состояния без изменения иерархии компонентов. Это облегчает обмен ссылками между многими компонентами или всей системы в целом. Так же классовые компоненты выглядят достаточно страшно, мы описываем методы жизненного цикла componentDidMount
/shouldComponentUpdate
/componentDidUpdate
, состояние компонента, создаем методы для работы с состоянием/стором, биндим методы для экземпляра компонента и так можно продолжать до бесконечности. Обычно такие компоненты выходят за рамки x строк, где x достаточно сложно для понимания.
Хуки позволяют делать тоже самое разбивая логику между компонентами на маленькие функции и использовать их внутри компонентов.
Классы сложны для людей и для машин
В наблюдении facebook классы являются большим препятствием при изучении React. Вам необходимо понять как работает this
, а он не работает так как в остальных языках программирования, так же следует помнить о привязке обработчиков событий. Без стабильных предложений синтаксиса код выглядит очень многословно. Люди прекрасно понимают паттерны props/state и так называемый top-down data flow, но достаточно сложно понимают классы. Особенно если не ограничиваться шаблонами, не так давно ребята из реакта эксперементировали с компоновкой компонентов c Prepack и увидели многообещающие результаты, но тем не менее компоненты класса позволяют создавать непреднамеренные плохие паттерны, которые заставляют эти оптимизации исчезать, так же классы не очень хорошо мигрируют и при горячей перезагрузке классы делают ее ненадежной. В первую очередь ребятам хотелось предоставить API которое поддерживает все оптимизации и отлично работает с горячей перезагрузкой.
Глянем на хуки
State hook
Код ниже рендерит параграф и кнопку и если мы нажмем на кнопку то значение в параграфе будет инкрементировано.
import { useState } from 'react';
function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
return (
You clicked {count} times
);
}
Отсюда можно сделать вывод что данный хук работает похоже с таким понятием как state
.
Немного детальнее метод useState
принимает один аргумент, это значение по умолчанию и возвращает кортеж (tuple) в котором есть само значение и метод для его изменения, в отличие от setState, setCount не будет делать merge значений, а просто обновит его. Так же мы можем использовать множественное объявление состояний, например:
function ExampleWithManyStates() {
// Declare multiple state variables!
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
// ...
}
таким образом мы создаем сразу несколько состояний и нам не нужно думать о том, чтобы их как то декомпозировать. Таким образом можно выделить, что хуки это функции которые позволяют «подключаться» к фишкам классовых компонентов, так же хуки не работают внутри классов, это важно запомнить.
Effect hook
Часто в классовых компонентах, мы делаем side effect функции, например подписываемся на события или делаем запросы за данными, обычно для этого мы используем методы componentDidMount
/componentDidUpdate
import { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
return (
You clicked {count} times
);
}
Когда мы вызываете useEffect
мы говорим реакту сделать 'side effect' после обновления изменений в DOM дереве. Эффекты объявляются внутри компонента, поэтому имеют доступ к props/state. Причем их мы можем точно так же создавать сколько угодно.
function FriendStatusWithCounter(props) {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
// ...
Сразу же стоит обратить внимание на второй side effect в нем мы возвращаем функцию, делаем мы это для того, чтобы выполнить какие то действия после того как компонент выполняет unmount, в новом api это называют эффекты с очисткой. Остальные эффекты могут возвращать, что угодно.
Правила хуков
Хуки это просто javascript функции, но они требуют всего двух правил:
- Выполнять хуки следует в самом верху иерархии функции (это означает, что не следует вызывать хуки в условиях и циклах, иначе реакт не может гарантировать порядок выполнения хуков)
- Вызывать хуки только в React функциях или функциональных компонентах или вызывать хуки из кастомных хуков (это ниже).
Для следования этим правилам ребята из команды реакта создали линтер плагин который выдаст ошибку если вы вызываете хуки в компонентах класса или в циклах и условиях.
Кастомные хуки
В тоже время нам хочется переиспользовать логику stateful компонентов, обычно для этого используют либо HOC либо render props паттерны, но они создают дополнительный объем нашего приложения.
Например опишем следующую функцию:
import { useState, useEffect } from 'react';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(() => {
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
Осознайте этот код, это будет кастомный хук, который мы можем вызывать в различных компонентах. Например так:
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
или так
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
{props.friend.name}
);
}
В любом случае, мы переиспользуем состояние компонента, каждый вызов функции useFriendStatus
создает изолированное состояние. Так же стоит отметить, что начало этой функции начинается со слова use это говорит о том, что это хук. Советуем соблюдать этот формат. Вы можете писать кастомные хуки на что угодно, анимации/подписки/таймеры и много многое другое.
Есть еще пара хуков.
useContext
useContext
позволяет использовать вместо renderProps обычное возвращаемое значение, в него следует передать контекст который мы хотим извлечь и он нам его вернет, таким образом мы можем избавиться от всех HOC, которые передавали context в props.
function Example() {
const locale = useContext(LocaleContext);
const theme = useContext(ThemeContext);
// ...
}
И теперь объект контекста мы можем просто использовать в возвращаемом значении.
useCallback
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
Как часто вам приходилось создавать компонент класса только для того, чтобы сохранить ссылку на метод? Этого больше не нужно делать, мы можем использовать useCallback и наши компоненты не будут перерисовываться потому что пришла новая ссылка на onClick.
useMemo
Возвращаем мемоизированное значение, мемоизированное значит вычисляется только тогда, когда один из аргументов поменялся, второй раз одно и тоже вычисляться не будет.
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
Да тут приходится дублировать значения в массиве, чтобы хук понял, что они не изменились.
useRef
useRef
возвращает мутируемое значение, где поле .current
будет инициализировано первым аргументом, объект будет существовать пока существует компонент.
Самый обычный пример при фокусе на input
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` points to the mounted text input element
inputEl.current.focus();
};
return (
<>
>
);
}
useImperativeMethods
useImperativeMethods
кастомизирует значение экземпляра который передается из родителя и использует ref напрямую. Как всегда следует избегать передачу ссылок на прямую и следует использовать forwardRef
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeMethods(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return ;
}
FancyInput = forwardRef(FancyInput);
В этом примере компонент который рендерит FancyInput
может вызывать fancyInputRef.current.focus()
.
useMutationEffect
useMutationEffect
очень похож на useEffect
за исключением того что он запускается синхронно на том этапе когда реакт изменяет значения DOM, прежде чем соседние компоненты будут обновлены, этот хук следует использовать для выполнения DOM мутаций.
Лучше предпочитать useEffect чтобы предотвратить блокировку визуальных изменений.
useLayoutEffect
useLayoutEffect
так же похож на useEffect
за исключением того, что запускается синхронно после всех обновлений DOM и синхронного ре-рендера. Обновления запланированные в useLayoutEffect
применяются синхронно, до того как браузер получит возможность отрисовать элементы. Так же следует стараться использовать стандартный useEffect
чтобы не блокировать визуальные изменения.
useReducer
useReducer
— это хук для создания редюсера который возвращает состояние и возможность диспатчить изменения:
const [state, dispatch] = useReducer(reducer, initialState);
Если вы понимаете как работает Redux, то вы понимаете как работает useReducer
. Тот же пример, что был с счетчиком сверху только через useReducer
:
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'reset':
return initialState;
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
}
}
function Counter({initialCount}) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
>
);
}
Так же useReducer принимает 3 аргумент, это action
который должен выполнятся при инициализации редюсера:
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'reset':
return {count: action.payload};
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
}
}
function Counter({initialCount}) {
const [state, dispatch] = useReducer(
reducer,
initialState,
{type: 'reset', payload: initialCount},
);
return (
<>
Count: {state.count}
>
);
}
Так же мы можем создать контекст в данным редюсером и через хук useContext
использовать его во всем приложении, это остается на домашнее задание.
Подводя итог
Хуки достаточно мощный подход по решению wrapper-hell и решают несколько проблем, но все их можно свети с одному определению передача ссылок. Уже сейчас начинают появляться сборники хуков по использованию или этот сборник. Более подробнее с хуками можно познакомиться в документации