Хабрахабр

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

8, с которой нам стали доступны хуки. Относительно недавно вышла версия React.js 16. Концепция хуков позволяет писать полноценные функциональные компоненты, используя все возможности React, и позволяет делать это во многом более удобно, чем мы это делали с помощью классов.

Многие восприняли появление хуков с критикой, и в этой статье я хотел бы рассказать о некоторых важных преимуществах, которые нам дают функциональные компоненты с хуками, и почему нам стоит перейти на них.

Это не очень важно для понимания примеров в этой статье, достаточно общего понимания работы React. Я намеренно не буду углубляться в детали использования хуков. Если вы хотите почитать именно на эту тему, информация о хуках есть в документации, и если эта тема будет интересна, я напишу статью подробнее о том когда, какие, и как правильно использовать хуки.

Хуки делают переиспользование кода удобнее

Что-то, что просто выведет несколько инпутов и позволит нам их редактировать. Давайте представим компонент, который рендерит простую форму.

Примерно так, если сильно упростить, этот компонент выглядел бы в виде класса:

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

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть