Относительно недавно вышла версия React.js 16.8, с которой нам стали доступны хуки. Концепция хуков позволяет писать полноценные функциональные компоненты, используя все возможности React, и позволяет делать это во многом более удобно, чем мы это делали с помощью классов.
Многие восприняли появление хуков с критикой, и в этой статье я хотел бы рассказать о некоторых важных преимуществах, которые нам дают функциональные компоненты с хуками, и почему нам стоит перейти на них.
Я намеренно не буду углубляться в детали использования хуков. Это не очень важно для понимания примеров в этой статье, достаточно общего понимания работы React. Если вы хотите почитать именно на эту тему, информация о хуках есть в документации, и если эта тема будет интересна, я напишу статью подробнее о том когда, какие, и как правильно использовать хуки.
Хуки делают переиспользование кода удобнее
Давайте представим компонент, который рендерит простую форму. Что-то, что просто выведет несколько инпутов и позволит нам их редактировать.
Примерно так, если сильно упростить, этот компонент выглядел бы в виде класса:
class Form extends React.Component { state = { // Значения полей fields: {}, }; render() { return ( <form> {/* Рендер инпутов формы */} </form> ); }; }
Теперь представим, что мы хотим автоматически сохранять значения полей при их изменении. Предлагаю опустить объявления дополнительных функций, вроде shallowEqual и debounce.
class Form extends React.Component { constructor(props) { super(props); this.saveToDraft = debounce(500, this.saveToDraft); }; state = { // Значения полей fields: {}, // Данные, которые нам нужны для сохранения черновика draft: { isSaving: false, lastSaved: null, }, }; saveToDraft = (data) => { if (this.state.isSaving) { return; } this.setState({ isSaving: true, }); makeSomeAPICall().then(() => { this.setState({ isSaving: false, lastSaved: new Date(), }) }); } componentDidUpdate(prevProps, prevState) { if (!shallowEqual(prevState.fields, this.state.fields)) { this.saveToDraft(this.state.fields); } } render() { return ( <form> {/* Рендер информации о том, когда был сохранен черновик */} {/* Рендер инпутов формы */} </form> ); }; }
Тот же пример, но с хуками:
const Form = () => { // Стейт для значений формы const [fields, setFields] = useState({}); const [draftIsSaving, setDraftIsSaving] = useState(false); const [draftLastSaved, setDraftLastSaved] = useState(false); useEffect(() => { const id = setTimeout(() => { if (draftIsSaving) { return; } setDraftIsSaving(true); makeSomeAPICall().then(() => { setDraftIsSaving(false); setDraftLastSaved(new Date()); }); }, 500); return () => clearTimeout(id); }, [fields]); return ( <form> {/* Рендер информации о том, когда был сохранен черновик */} {/* Рендер инпутов формы */} </form> ); }
Как мы видим, разница пока не очень большая. Мы поменяли стейт на хук useState и вызываем сохранение в черновик не в componentDidUpdate, а после рендера компонента с помощью хука useEffect.
Отличие, которое я хочу здесь показать (есть и другие, о них будет ниже): мы можем вынести этот код и использовать в другом месте:
// Хук useDraft вполне можно вынести в отдельный файл const useDraft = (fields) => { const [draftIsSaving, setDraftIsSaving] = useState(false); const [draftLastSaved, setDraftLastSaved] = useState(false); useEffect(() => { const id = setTimeout(() => { if (draftIsSaving) { return; } setDraftIsSaving(true); makeSomeAPICall().then(() => { setDraftIsSaving(false); setDraftLastSaved(new Date()); }); }, 500); return () => clearTimeout(id); }, [fields]); return [draftIsSaving, draftLastSaved]; } const Form = () => { // Стейт для значений формы const [fields, setFields] = useState({}); const [draftIsSaving, draftLastSaved] = useDraft(fields); return ( <form> {/* Рендер информации о том, когда был сохранен черновик */} {/* Рендер инпутов формы */} </form> ); }
Теперь мы можем использовать хук useDraft, который только что написали, в других компонентах! Это, конечно, очень упрощенный пример, но переиспользование однотипного функционала — очень полезная возможность.
Хуки позволяют писать более интуитивно-понятный код
Представьте компонент (пока в виде класса), который, например, выводит окно текущего чата, список возможных получателей и форму отправки сообщения. Что-то такое:
class ChatApp extends React.Component { state = { currentChat: null, }; handleSubmit = (messageData) => { makeSomeAPICall(SEND_URL, messageData) .then(() => { alert(`Сообщение в чат ${this.state.currentChat} отправлено`); }); }; render() { return ( <Fragment> <ChatsList changeChat={currentChat => { this.setState({ currentChat }); }} /> <CurrentChat id={currentChat} /> <MessageForm onSubmit={this.handleSubmit} /> </Fragment> ); }; }
Пример очень условный, но для демонстрации вполне подойдет. Представьте такие действия пользователя:
- Открыть чат 1
- Отправить сообщение (представим, что запрос идет долго)
- Открыть чат 2
- Получить сообщение об успешной отправке:
- "Сообщение в чат 2 отправлено"
Но ведь сообщение отправлялось в чат 1? Так произошло из-за того, что метод класса работал не с тем значением, которое было в момент отправки, а с тем, которое было уже на момент завершения запроса. Это не было бы проблемой в таком простом случае, но исправление такого поведения во-первых, потребует дополнительной внимательности и дополнительной обработки, и во-вторых, может быть источником багов.
В случае с функциональным компонентом поведение отличается:
const ChatApp = () => { const [currentChat, setCurrentChat] = useState(null); const handleSubmit = useCallback( (messageData) => { makeSomeAPICall(SEND_URL, messageData) .then(() => { alert(`Сообщение в чат ${currentChat} отправлено`); }); }, [currentChat] ); render() { return ( <Fragment> <ChatsList changeChat={setCurrentChat} /> <CurrentChat id={currentChat} /> <MessageForm onSubmit={handleSubmit} /> </Fragment> ); }; }
Представьте те же действия пользователя:
- Открыть чат 1
- Отправить сообщение (запрос снова идет долго)
- Открыть чат 2
- Получить сообщение об успешной отправке:
- "Сообщение в чат 1 отправлено"
Итак, что же поменялось? Поменялось то, что теперь для каждого рендера, для котрого отличается currentChat мы создаем новый метод. Это позволяет нам совсем не думать о том, поменяется ли что-то в будущем — мы работаем с тем, что имеем сейчас. Каждый рендер компонента замыкает в себе все, что к нему относится.
Хуки избавляют нас от жизненного цикла
Этот пункт сильно пересекается с предыдущим. React — библиотека для декларативного описания интерфейса. Декларативность сильно облегчает написание и поддержку компонентов, позволяет меньше думать о том, что было бы нужно сделать императивно, если бы мы не использовали React.
Несмотря на это, при использовании классов, мы сталкиваемся с жизненным циклом компонента. Если не углубляться, это выглядит так:
- Монтирование компонента
- Обновление компонента (при изменении
stateилиprops) - Демонтирование компонента
Это кажется удобным, но я убежден в том, что это удобно исключительно из-за привычности. Этот подход не похож на React.
Вместо этого, функциональные компоненты с хуками позволяют нам писать компоненты, думая не о жизненном цикле, а о синхронизации. Мы пишем функцию так, чтобы ее результат однозначно отражал состояние интерфейса в зависимости от внешних параметров и внутреннего состояния.
Хук useEffect, который многими воспринимается как прямая замена componentDidMount, componentDidUpdate и так далее, на самом деле предназначен для другого. При его использовании мы как бы говорим реакту: "После того, как отрендеришь это, выполни, пожалуйста, эти эффекты".
Вот хороший пример работы компонента со счетчиком кликов из большой статьи про useEffect:
- React: Скажи мне, что отрендерить с таким состоянием.
- Ваш компонент:
- Вот результат рендера:
<p>Вы кликнули 0 раз</p>. - И еще, пожалуйста, выполни этот эффект, когда закончишь:
() => { document.title = 'Вы кликнули 0 раз' }.
- Вот результат рендера:
- React: Окей. Обновляю интерфейс. Эй, брайзер, я обновляю DOM
- Браузер: Отлично, я отрисовал.
- React: Супер, теперь я вызову эффект, который получил от компонента.
- Запускается
() => { document.title = 'Вы кликнули 0 раз' }
- Запускается
Намного более декларативно, не правда ли?
Итоги
React Hooks позволяют нам избавиться от некоторых проблем и облегчить восприятие и написание кода компонентов. Нужно просто поменять ментальную модель, которую мы на них применяем. Функциональные компоненты по сути — функции интерфейса от параметров. Они описывают все так, как оно должно быть в любой момент времени, и помогают не думать о том, как реагировать на изменения.
Да, иногда нужно научиться их использовать правильно, но точно так же и компоненты в виде классов мы научились применять не сразу.
