Функциональные компоненты с React Hooks. Чем они лучше?

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

Да, иногда нужно научиться их использовать правильно, но точно так же и компоненты в виде классов мы научились применять не сразу.

Оставить комментарий