Я более чем уверен, что какие-то из предложенных решений вы могли видеть, изучая исходники в различных проектах. Скажу больше, есть даже целые npm-пакеты, которые уже имеют готовые решения (пример: https://github.com/streamich/react-use). Казалось бы, зачем тогда рассматривать похожие реализации?
Но в жизни часто возникают ситуации, когда что-то из готового решения необходимо кастомизировать под реалии текущего проекта. А еще понимание того, как работает инструмент, позволяет не только сделать его кастомизацию быстро и без ошибок, но также способствует развитию разработчика и более глубокому пониманию работы фреймворка.
Для удобства я создал отдельный репозиторий со всеми хуками, что будут обсуждаться (https://github.com/Jock96/react-useful-tools). В этой статье начнем с самых простых и типовых инструментов, но постепенно будем переходить к более сложным.
Храним предыдущее состояние (usePrevious)

Данный хук позволяет хранить в себе предыдущее состояние любого переданного значения. Не триггерит отрисовку, так как не хранит и не записывает состояние (useState). Вместо этого присваивает значения с помощью рефы (useRef). Для поддержки тайпскрипта хук является дженериком (T). Строку с присваиванием можно не оборачивать в хук эффекта, так как это равносильное выражение. Возвращать можно либо ref, либо ref.current — тут как удобно конечному пользователю. По моему мнению, если при использовании хука будет лучше сохранить передачу ref в первозданном виде, то снимается лишняя когнитивная нагрузка. Происходит это из-за подсознательного понимания пользователя о том, что он работает с мутабельным объектом. Однако не исключено и использование только значения ref.current, для более лаконичного вида. Делать это следует лучше тогда, когда все в команде знают внутреннее устройство функции.
В папке с хуками можно увидеть модификацию, которая называется useLastValue.

Такая реализация позволяет нам учитывать только отличные друг от друга изменения. Это поможет исключить ситуацию, когда мы триггеримся на одни и те же изменения либо нам критично важно иметь только уникальные значения в цепочке зависимостей наших функций.
Когда полезны эти хуки? Тогда, когда нам необходимо реагировать на разницу между предыдущим и текущим значениями либо нужны предыдущие данные для вычисления. Пример — сравнение при инвалидации кеша, промежуточные состояния при смене view, реакция на разницу между данными.
Отслеживаем первый рендер (useFirstRender)

Данный хук позволяет нам выявить состояние вью, когда оно находится на этапе первой отрисовки. Инструмент отлично подсвечивает, насколько сильно оптимизировано приложение, так как при использовании в сочетании с предыдущими хуками вы сможете увидеть, сколько раз перерисовывается страница при первом входе пользователя.
Когда полезен хук? Когда необходимо выполнить реакцию на первый рендер — к примеру, показать экран загрузки или скелетоны определенных компонентов, для которых еще не был вызван запрос на получение данных.
Управляем выбранными элементами в списках (useSelection)

Данный хук позволяет нам управлять выбранными элементами списка и переключать режимы выбора (включено/исключено). Очень гибкий инструмент и при сочетании с грамотным API бэкенда позволяет оптимально обрабатывать большие и малые выборки без особой сложности. Для выбора уникальных ключей используется кастомная функция на основе Set, по факту это аналог uniq из Lodash и используется, чтобы не тянуть лишние зависимости в проект. Вместо этого можно использовать любой другой вариант или библиотеку.
Когда полезен хук? Когда необходимо удобно и просто управлять выбранными элементами в детерминированных и недетерминированных списках.
Управление сортировкой списков (useSort)

Данный хук позволяет нам сортировать списки и реагировать на изменение ключей сортировки. Крайне полезно иметь такой хук в случае отсутствия сортировки на стороне сервера. В хук передаем сортируемый список и опциональную функцию для сортировки по заданному ключу.
Когда полезен хук? Когда необходимо обрабатывать сортировку списков на стороне клиента.
Храним состояние в localStorage (useLocalStorageState)

Данный хук позволяет автоматически сохранять данные в localStorage и загружать их в случае переключения или обновления страницы. При использовании хука мы улучшаем UX, так как при различных манипуляциях с данными и работе с приложением вне данного окна мы можем:
- вернуться и получить данные в том виде, в котором мы оставили их в последний период нашей работы;
- в случае переключения вью и мутации данных нам не настолько критично отправлять их каждый раз на сервер, так как мы можем использовать мутированные данные из кеша до ответа сервера и подстраивать вью под состояние.
По сути, хук приближает наше приложение к PWA, однако существуют и минусы:
- переполнение localStorage;
- коллизия ключей localStorage;
- ручное очищение localStorage.
На данные риски есть вполне стандартные рекомендации: трезво оценивать, необходимо ли кешировать выбранные данные и всегда следить за чистотой localStorage, а также уникальностью ваших ключей.
Интерфейс хука позволяет нам:
- установить начальное состояние;
- реагировать на восстановление данных из localStorage;
- отключать загрузку/выгрузку данных в/из localStorage (превращает наш хук в обычный useState).
Когда полезен хук? Когда мы хотим приблизить наш модуль/приложение к PWA и повысить UX, а также есть потребность в кешировании данных.
Храним состояние в строке поиска (useQueryParamsState)

Данный хук позволяет хранить данные в строке браузера (queryParams) и использовать эти данные между компонентами на странице. Здесь можно провести аналогию между сочетанием useContext и useState, где:
- контекстом является текущая страница;
- состоянием является queryParams в URL.
В отличие от предыдущего хука, больше направлен на улучшение UX и реактивности, нежели на работу с кешем, хотя по факту кеширует данные (в пределах одной страницы). Улучшение UX происходит в тот момент, когда необходимо:
- перезагрузить страницу и увидеть состояние до рефреша;
- отправить ссылку страницы пользователю, чтобы он увидел то же состояние, что и вы, без лишних манипуляций с вью.
Риски, связанные с использованием хука:
- ручное очищение или манипуляции с данными в строке браузера;
- ограниченный размер URL;
- безопасность используемых данных.
Для устранения рисков всегда важно:
- сериализовывать данные тем энкодером, что подходит для безопасности вашего приложения и эффективно сжимает данные;
- следить за объемом данных, которые вы храните в строке;
- оптимизировать количество хранимых данных, разграничивая важные и второстепенные;
- реагировать на ручное изменение данных в строке браузера (логирование, инвалидация, обработка исключений).
В качестве инструмента работы с навигацией используется React Router, но никто не мешает адаптировать данный инструмент под свои реалии. В качестве примера (usePureQueryParamsState) — использование нативных возможностей без роутера.
Когда полезен хук? Когда мы хотим повысить реактивность, улучшить UX и шеринг ресурсов. Пример использования — хранение состояния фильтров и передача ссылки другому пользователю.
Управляем стратегией кеширования (useCacheStrategy)

Данный хук позволяет нам обрабатывать сценарии использования кеша или реальных данных в различных ситуациях. В сочетании с useLocalStorageState (или useQueryParamsState), а также нашего фетчера реализуемы сценарии:
- кеш в первую очередь (cache first);
- данные запроса в первую очередь (network first);
- только кеш (cache only);
- только данные запроса (network only);
- синхронный с данными кеш (synchronized cache) и др.
Когда полезен хук? Когда появляется кеш в приложении и необходимо выбирать стратегию работы с данными, а также прорабатывать инвалидацию кеша.
В заключение хотелось бы сказать, что этот набор инструментов — только малая часть накопленного опыта, которым я хочу поделиться в будущих статьях. Поощряется копипаст, модификация и активное использование в боевых проектах. Позже создам отдельный npm-пакет с типизацией для более удобного использования. Жду фидбэка, ведь я, так же как и вы, расту и развиваюсь каждый день!