Относительно недавно вышла версия 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 позволяют нам избавиться от некоторых проблем и облегчить восприятие и написание кода компонентов. Нужно просто поменять ментальную модель, которую мы на них применяем. Функциональные компоненты по сути — функции интерфейса от параметров. Они описывают все так, как оно должно быть в любой момент времени, и помогают не думать о том, как реагировать на изменения.
Да, иногда нужно научиться их использовать правильно, но точно так же и компоненты в виде классов мы научились применять не сразу.
Автор: kshshe