Меня зовут Артём Березин, я разработчик нескольких внутренних сервисов Яндекса. Последние полгода я активно работал с React Hooks. По ходу дела возникали некоторые сложности, с которыми приходилось бороться. Теперь хочу поделиться этим опытом с вами. В докладе я разобрал React Hook API с практической точки зрения — зачем нужны хуки, стоит ли переходить, что лучше учитывать при портировании. В процессе перехода легко наделать ошибок, но избежать их тоже не так сложно.
— Хуки — это просто еще один способ описывать логику ваших компонентов. Он позволяет добавить к функциональным компонентам некоторые возможности, ранее присущие только компонентам на классах.
Прежде всего это поддержка внутреннего состояния, затем — поддержка побочных эффектов. Например — сетевых запросов или запросов к WebSocket: подписка, отписка от каких-то каналов. Или, возможно, речь о запросах к каким-то другим асинхронным или синхронным API браузера. Также хуки дают нам доступ к жизненному циклу компонента, к его началу жизни, то есть монтированию, к обновлению его пропсов и к его смерти.
Наверное, проще всего проиллюстрировать в сравнении. Перед вами здесь самый простой код, который только может быть с компонентом на классах. Компонент что-то меняет. Это обычный счетчик, который можно увеличивать или уменьшать, всего одно поле в state. В общем, я думаю, что если вы знакомы с React, код для вас совершенно очевиден.
Подобный компонент, выполняющий абсолютно ту же самую функцию, но написанный на хуках, выглядит гораздо компактнее. По моим подсчетам, в среднем при портировании с компонентов на классах в компоненты на хуках код уменьшается примерно в полтора раза, и это радует.
Пару слов о том, как работают хуки. Хук — глобальная функция, которая объявлена внутри React и вызывается при каждом рендере компонента. React отслеживает вызовы этих функций и может изменить ее поведение или решить, что она должна вернуть.
На использование хуков накладываются некоторые ограничения, которые их отличают от обычных функций. В первую очередь, их нельзя использовать в компонентах на классах, просто действует такое ограничение, потому что они созданы не для них, а для функциональных компонентов. Хуки нельзя вызывать внутри внутренних функций, внутри циклов, условий. Только на первом уровне вложенности, внутри функций компонента. Это ограничение накладывает сам React, чтобы получить возможность отслеживать, какие хуки были вызваны. И он их складывает в определенном порядке у себя в мозгах. Потом, если этот порядок внезапно изменится или какие-то пропадут, возможны сложные, трудноуловимые, трудноотлаживаемые ошибки.
Но если у вас достаточно сложная логика и хотелось бы использовать, например, хуки внутри хуков, то, скорее всего, это признак, что вам стоит вынести хук. Допустим, вынести несколько связанных между собой хуков в отдельный кастомный хук. И внутри него можно использовать другие кастомные хуки, тем самым выстраивая иерархию хуков, выделяя туда общую логику.
Хуки дают некоторые преимущества по сравнению с классами. Прежде всего, как следует из предыдущего, используя кастомные хуки, можно шарить логику гораздо проще. Раньше, используя подход с Higher Order Components, мы выкладывали туда какую-то шаренную логику, и она являлась оберткой над компонентом. Теперь мы эту логику закладываем внутрь хуков. Тем самым дерево компонентов уменьшается: уменьшается его вложенность, и для React становится проще отслеживать изменения компоненты, пересчитывать дерево, пересчитывать виртуальный DOM и т. д. Тем самым решается проблема так называемого wrapper-hell. Тем, кто работает с Redux, я думаю, это хорошо знакомо.
Код, написанный с использованием хуков, гораздо легче поддается минимизации современными минимизаторами типа Terser или старым UglifyJS. Дело в том, что нам не нужно сохранять имена методов, не нужно думать о прототипах. После транспиляции, если target стоит ES3 или ES5, у нас обычно появляется куча прототипов, которые патчатся. Здесь всего этого не нужно делать, поэтому проще минимизировать. И, как следствие неиспользования классов, нам не нужно думать про this. Для новичков это часто большая проблема и, наверное, одна из главных причин багов: мы забываем про то, что this может быть window, что надо забиндить метод, например, в конструкторе или каким-то еще способом.
Также использование хуков позволяет выделить логику, управляющую каким-то одним побочным эффектом. Раньше эту логику, особенно, когда у нас несколько побочных эффектов у компонента, приходилось разбивать на разные методы жизненного цикла у компонента. И, так как появились хуки минимизации, появился React.memo, теперь функциональные компоненты поддаются мемоизации, то есть у нас этот компонент не будет пересоздан или обновлен, если его пропсы не изменились. Раньше такого нельзя было делать, теперь можно. Все функциональные компоненты можно обернуть в memo. Также внутри появился хук useMemo, который мы можем использовать для того, чтобы вычислять какие-то тяжелые значения, или инстанцировать какие-то служебные классы только единожды.
Доклад будет неполным, если я не расскажу о каких-то базовых хуках. Прежде всего, это хуки управления состоянием.
В первую очередь — useState.
Пример похож на тот, что в начале доклада. useState — это функция, которая принимает начальное значение, и возвращает tuple из текущего значения и функции для изменения этого значения. Вся магия обслуживается React внутри. Мы же можем просто либо прочитать это значение, либо его изменить.
В отличие от классов мы можем использовать столько объектов стейта, сколько нам нужно, разбивает стейт на логические куски, чтобы их не смешивать в одном объекте, как в классах. И эти куски будут друг от друга совершенно изолированы: их можно менять независимо друг от друга. Результат, например, этого кода: мы меняем две переменные, вычисляем результат и выводим кнопочки, которые позволяют нам изменить первую переменную туда-сюда, и вторую переменную тоже туда-сюда. Запомните этот пример, потому что дальше мы будем делать похожую штуку, но, гораздо более сложную.
Есть такой useState на стероидах для любителей Redux. Он позволяет более консистентно изменять стейт с помощью редьюсера. Я думаю, что те, кто знаком с Redux, можно даже не объяснять, для тех, кто незнаком, расскажу.
Редьюсер — это функция, которая принимает стейт, и некий объект, называемый обычно action, который описывает то, как этот стейт должен поменяться. Точнее, он передает какие-то параметры, а внутри reducer уже решает, в зависимости от их параметров, как стейт поменяется, и в итоге, должен вернуться новый стейт, обновленный.
Примерно таким образом он используется в коде компонента. У нас хук useReducer, он принимает функцию reducer, и вторым параметром начальное значение стейта. Возвращает, так же, как и useState, текущий стейт, и функцию для его изменения — dispatch. Если передать в dispatch объект-action, мы вызовем изменение стейта.
Очень важный хук useEffect. Он позволяет добавлять побочные эффекты для компонента, давая альтернативу жизненному циклу. В данном примере мы используем простой способ с useEffect: это просто запрос каких-то данных с сервера, с API, например, и вывод этих данных на страничке.
У useEffect есть продвинутый режим, это когда функция, переданная в useEffect, возвращает какую-то другую функцию, то эта функция будет вызвана при следующем цикле, когда этот useEffect будет применен.
Забыл упомянуть, useEffect вызывается асинхронно, сразу после того, как применится изменение к DOM. То есть он гарантирует, что он будет выполнен после рендера компонента, и может привести к следующему рендеру, если какие-то значения изменятся.
Здесь мы с вами впервые встречаемся с таким понятием, как dependencies. Некоторые хуки — useEffect, useCallback, useMemo — вторым аргументом принимают массив значений, которые позволят сказать, что отслеживать. Изменения в этом массиве приводят к каким-то эффектам. Например, здесь гипотетически у нас какой-то компонент выбора автора из какого-то списка. И табличка с книжками этого автора. И при изменении автора, будет вызван useEffect. При изменении этого authorId будет вызван запрос, и загрузятся книжки.
Также вскользь упомяну такие хуки, как useRef, это альтернатива React.createRef, что-то похожее на useState, но изменения в ref не приводят к рендеру. Иногда удобно для каких-то хаков. useImperativeHandle позволяет нам объявить у компонента определенные «публичные методы». Если использовать useRef в родительском компоненте, то он может эти методы дернуть. Если честно, один раз пробовал в учебных целях, на практике не пригодилось. useContext — прямо хорошая штука, позволяет взять текущее значение из контекста, если провайдер где-то выше по уровню иерархии определил это значение.
Существует один способ оптимизации React-приложений на хуках, это мемоизация. Мемоизацию можно разделить на внутреннюю и внешнюю. Сначала о внешней.
Это React.memo, практически альтернатива классу React.PureComponent, который отслеживал изменение в пропсах и менял компоненты только тогда, когда изменились пропсы или стейт.
Здесь же похожая штука, правда, без стейта. Она тоже отслеживает изменения в пропсах, и если пропсы изменились, происходит ререндер. Если пропсы не менялись, компонент не обновляется, и мы на этом экономим.
Внутренние способы оптимизации. Прежде всего, это достаточно низкоуровневая штука — useMemo, редко используется. Она позволяет вычислять какое-то значение, и перевычислять его только, если указанные в зависимостях значения изменились.
Есть частный случай useMemo для функции, называется useCallback. Она, прежде всего, применяется для того, чтобы замемоизировать значение функциий-обработчиков событий, которые будут переданы в дочерние компоненты для того, чтобы эти дочерние компоненты могли не перерендериваться лишний раз. Используется она просто. Мы описываем некую функцию, оборачиваем ее в useCallback, и указываем, от каких переменных она зависит.
У многих возникает вопрос, а надо ли нам это? Надо ли нам хуки? Переезжаем или остаемся как раньше? Тут однозначного ответа нет, все зависит от предпочтений. В первую очередь, если вы прямо жестко завязаны на объектно-ориентированное программирование, если ваши компоненты, вы привыкли, что это класс, у них есть методы, которые можно дернуть, то, наверное, вам может показаться эта штука лишней. В принципе, мне так и показалось, когда я впервые услышал о хуках, что слишком как-то переусложнено, какая-то магия добавляется, и непонятно зачем.
Для любителей функциональщины, это, скажем, прямо must have, потому что хуки — это функции, и к ним применимы приемы функционального программирования. К примеру, их можно комбинировать или вообще делать что угодно, используя, например, библиотеки, типа Ramda, и тому подобных.
Так как мы избавились от классов, нам теперь нет необходимости привязывать контекст this к методам. Если использовать эти методы в качестве колбеков. Обычно, это была проблема, потому что нужно было не забыть забиндить их в конструкторе, либо использовать неофициальное расширение синтаксиса языка, такие arrow-functions в качестве property. Довольно распространенная практика. Я же использовал свой декоратор, что тоже, в принципе, экспериментально, на методы.
Есть разница в том, как устроен жизненный цикл, как им управлять. Хуки практически все действия с жизненным циклом связывают с хуком useEffect, который позволяет подписаться, как на рождение, так и на обновление компоненты, так и на его смерть. В классах же для этого приходилось переопределять несколько методов, типа componentDidMount, componentDidUpdate, и componentWillUnmount. Также метод shouldComponentUpdate теперь можно заменить на React.memo.
Есть достаточно небольшая разница в том, как обрабатывается состояние. Во-первых, у классов у нас один объект состояния. Мы должны были запихать туда все, что угодно. В хуках же мы можем разбить логическое состояние на какие-то куски, которыми нам удобно было бы оперировать раздельно.
setState() у компонентов на классах позволял указывать патч стейта, тем самым изменяя одно или несколько полей стейта. В хуках же мы должны менять весь стейт целиком, и это даже хорошо, потому что модно, что ли, использовать всякие Immutable-штуки и никогда не рассчитывать на то, что у нас объекты могут мутировать. Они всегда у нас новые.
Основная фишка классов, которой у хуков нет: мы могли подписаться на изменения состояния. То есть мы меняем состояние, и тут же подписываемся на его изменения, императивным способом обрабатывая что-то сразу после того, как изменения будут применены. В хуках это сделать просто так не получится. Это нужно очень интересным способом делать, я дальше расскажу.
И коротенько про функциональный способ обновления. Он и там и там работает, это когда функции изменения состояния принимают другую функцию, которая это состояние должна не изменить, а, скорее, создать. И если в случае с классовым компонентом он может вернуть нам какой-то patch, то в хуках мы должны вернуть новое значение целиком.
В общем, вы вряд ли получите ответ, переезжать или нет. Но я советую хотя бы попробовать, хотя бы для нового кода, чтобы прочувствовать. Когда я только начал работать с хуками, то сразу же выделил несколько кастомных хуков, удобных для себя, для своего проекта. В принципе, я пытался заменить некоторые возможности, которые у меня были реализованы через Higher Order Components.
useDismounted — для тех, кто знаком с RxJS, там есть возможность массово отписаться от всех Observable в рамках одного компонента, или в рамках одной функции, передав подписку каждого Observable в специальный объект, Subject, и когда он закрывается, все подписки отменяются. Это очень удобно, если компонент сложный, если там внутри много каких-то асинхронных операций через Observable, удобно сразу разом от всех отписываться, а не от каждого отдельно.
useObservable возвращает значение из Observable, когда оно там появится новое. Похожий хук useBehaviourSubject возвращает из BehaviourSubject. Его отличие от Observable в том, что у него изначально есть какое-то значение.
Удобный кастомный хук useDebouncedValue позволяет нам организовать, например, саджест для поисковой строки, чтобы не на каждое нажатие клавиши что-то отправлять на сервер, а подождать, пока пользователь закончит вводить.
Два похожих хука. useWindowResize возвращает текущие актуальные значения для размеров окна. Следующий хук для позиции скролла — useWindowScroll. Я их использую для пересчета некоторых pop-up или модальных окон, если там какие-то сложные штуки, которые с CSS просто так не делаются.
И такой небольшой хук для реализации горячих клавиш, которые компонент, когда он присутствует на странице, он подписан на какую-то горячую клавишу. Когда он умирает, происходит автоматическая отписка.
В чем удобны вообще вот эти кастомные хуки? В том, что мы можем внутрь хука запихать отписку, и нам можно не думать о том, чтобы отписываться вручную где-то в том компоненте, где этот хук используют.
Не так давно мне кинули ссылочку на библиотеку react-use, и оказалось, что большая часть вот этих кастомных хуков там уже реализовано. И я писал велосипед. Это иногда полезно, но в дальнейшем, наверное, скорее всего, я их выкину, и использую react-use. И вам советую тоже посмотреть, если собираетесь использовать хуки.
Собственно, главная цель доклада — показать то, как неправильно писать, какие проблемы могут быть и как их избежать. Самое первое, наверное, с чем сталкивается любой, кто изучает эти хуки и пытается что-то написать, это использовать useEffect неправильно. Здесь тот код, подобный которому 100% каждый писал, если пробовал хуки. Он связан с тем, что useEffect поначалу воспринимается ментально, как альтернатива componentDidMount. Но, в отличие от componentDidMount, который вызывается единожды, useEffect вызывается при каждом рендере. И ошибка здесь в том, что он изменяет, допустим, перемену data, и при этом ее изменение приводит к ререндеру компонента, как следствие, эффект будет перезапрошен. Тем самым мы получаем бесконечную череду AJAX запросов на сервер, и компонент сам себя постоянно обновляет, обновляет, обновляет.
Исправить его очень просто. Нужно вот сюда добавить пустой массив тех зависимостей, от которых он зависит, и изменения в которых будут перезапускать эффект. Если у нас здесь указан пустой список зависимостей, то эффект, соответственно, не будет перезапускаться. Это не какой-то хак, это базовая особенность, что ли, использования useEffect.
Допустим, мы исправили. Теперь немножко усложнили. У нас есть компонент, который рендерит что-то, что нужно взять из сервера по какому-то айдишнику. В этом случае, в принципе, все работает нормально до тех пор, пока мы не изменим entityId в родителе, возможно, для вашего компонента это и не актуально.
Но скорее всего, если он изменится или появится необходимость его изменить, а у вас на странице окажется старый компонент и выяснится, что он не обновляется, то лучше добавить вот сюда entityId, в качестве зависимости, вызвав обновление, актуализацию данных.
Более сложный пример с useCallback. Здесь, на первый взгляд, все хорошо. Мы имеем некую страничку, у которой есть какой-то countdown timer, или, наоборот, timer просто, который тикает. И, например, список хостов, и сверху фильтры, которые позволяют этот список хостов фильтровать. Ну, maintenance здесь добавлен просто как для иллюстрации часто изменяющегося значения, который переводит к рирендеру.
И в данном случае, проблема в том, что изменение внутри maintenance будет приводить к ререндеру, в том числе, и объекта фильтров, потому что фильтры подписываются на onChange. Туда мы передаем функцию onChange, которая каждый раз при каждом рендере будет новая. Тем самым, если HostFilters какой-то сложный, например, в нашем случае это куча dropdown, в которые внутри данные загружены. И если он будет перерендериваться лишний раз, то производительность может просесть. Появятся лаги, или просто опять же бесконечная череда запросов.
Здесь обязательно нужно этот onChange просто обернуть в useCallback. В нашем случае он ни от чего не зависит, поэтому в конце пустой массив указываем.
Есть еще некоторые сложности, которые я выделил. Они, возможно, больше мои. То есть эти сложности отвергает Facebook, как создатель React. Они говорят, что, типа, все хорошо, все о'кей. Но все равно некое смущение, что ли, или confusing бывает.
Что сразу бросается в глаза? Так как у нас теперь функциональный компонент — это не просто кусок рендера, а есть какая-то логика и, возможно, внутри указана куча всяких функций, то все эти функции и объекты, скорее всего, будут использованы только один раз или редко когда меняться. Но при этом инициализироваться они будут на каждый рендер.
Например, здесь выделено желтым то, что будет каждый раз при каждом рендере загружаться в память, то есть будет функция создаваться, а потом она будет не использоваться, потому что не нужна она, допустим. И получается, Garbage Collector вынужден ее очищать после каждого рендера, или когда ему захочется. И это, наверно, моя главная претензия к хукам, и я не знаю, как ее решить радикально. Есть определенные хаки, когда, допустим, сильно может помочь вынос логики в reducer, который как бы внешняя функция, и там можно большую часть логику по изменению стейта вынести. Но, все равно мы не решили принципиально.
Еще есть такая проблема, она очень нервирует что ли, или вызывает фрустрацию. Когда у нас меняется какое-то значение, например, мы делаем setValue для какого-то значения, то мы не можем в хуках подписаться на изменение этого значения напрямую, как в setState можно было раньше вторым аргументом. Нам нужно как-то декларативно подписаться через изменения зависимости у useEffect.
Этот же useEffect может вызвать какой-то другой эффект, или изменение каких-то других переменных, которые, в свою очередь, вызовет еще один useEffect. И эта цепочка взаимосвязанных useEffect очень сложно кладется в голову, ее сложно отлаживать. И зачастую мне это очень сильно напоминает ситуацию, когда в старых фреймворках, типа Backbone, приходилось подписываться: допустим, у нас есть классы моделей, классы коллекций, они бросают постоянно разные события на изменение каких-то своих кусков. И мы подписываемся на эти события, потом можем их скомбинировать, бросить какое-то еще одно событие, более глобальное. Оно уже вызовет где-то еще другое событие. И, в общем, мы получаем такое событие внутри события, которое вызывает другое событие, и это тоже очень-очень сложно отлавливать. И, вроде бы, компонент поменялся, а какую плеяду, скажем, других изменений он вызывает, не ясно. Здесь эта же проблема сохранилась.
И забавная такая, даже не претензия, а замечание. Иногда, когда мы выносим нашу логику в кастомные хуки, нам хочется эту логику переиспользовать. Но мы не можем это сделать в компонентах старых на классах, потому что они не поддерживают хуки. В моем случае это был довольно сложный компонент, куда можно было вбивать данные. Он искал, его можно было выбирать, типа, dropdown такой сложный. Он был большой, давно еще написанный на классах. Я для другого dropdown сделал pop-up, который автоматом использовал хуки useWindowScroll, useWindowResize и пересчитывал свой размерчик в зависимости от высоты экрана, очень удобно. Я хотел эту же логику использовать для этого компонента, который у меня сложный на классах, — не вышло, пришлось его переписывать.
И вот эта необходимость переписывать компоненты каждый раз, вызывает определенную сложность, что ли. Получается, мы и платим технический долг по переписыванию, но, с другой стороны, нам хочется заниматься продуктовыми фичами, а приходится переливать из пустого в порожнее. Но, наверное, это скорее плюс, потому что это дает более чистый, более единообразный код, и больше заставляет нас портировать старый код на новый.
Время еще есть, и есть небольшой раздел «Бонус», связанный с хуками. Я, как и многие в Яндексе, пишу на TypeScript и использую строгую типизацию. Есть определенная проблема в использовании редьюсеров. Она заключается в том, что reducer в Redux когда, он принимает объект action. Так вот, структура этого action может быть совершенно разной, в зависимости от типа action. И сделать так, чтобы работал автокомплит, и другие плюшки статической типизации, довольно сложно.
Я нашел следующий способ. Прежде всего, мы объявляем все перечисления со всеми типами action. Можем сделать вот так просто, тогда это будут числа просто, IncrementA это 0, потом 1, 2, и так далее. Можем для удобства отладки указать их в виде строк. Естественно, они не должны пересекаться, но, в принципе, вам компилятор не даст сделать одинаковые строки. Потом для каждого типа action мы описываем структуру этого action, расширяя его какими-то полями и обязательно указывая тип. Потом создаем UnionType “Action”, где мы перечисляем через вертикальную черту все типы данных, которые туда складываются, какими могут быть action. Это первый трюк.
Второй трюк — вычисление типа на основе значения. Например, здесь есть initialState, просто объект. И по идее, надо описать структуру этого объекта еще каким-нибудь одним интерфейсом. Но можно переложить эту задачу на TypeScript. Он умеет сам вычислять значения. Например, здесь я определяю typeState только для чтения, вычисляемый из initialState.
Дальше эти типы легко использовать в reducer. Мы передаем State, Action, и дальше происходит магия: мы используем switch по action.type. В TypeScript есть такая фича при использовании UnionType: он может вычислить допустимый тип внутри case, если эти типы отличаются значением какого-то поля, в нашем случае type. И мы как раз получаем возможность обращаться к полям этого action нужного нам типа.
Естественно, это удобно: прежде всего, это валидация по типам, подсказки автокомплита и все такое. И рефакторинг.
Как это применять? В принципе, при использовании особой разницы нет. Был предыдущий пример с небольшим калькулятором. Здесь мы делаем то же самое, только через reducer. Точнее, мы определяем action creator с типами, которые нам нужны, и потом просто отправляем их в dispatch.
Еще хочу пропагандировать использование extension официального реактовского Dev Tools. Они добавили поддержку хуков и позволяют очень классно показывать все дерево хуков. Особенно полезно для кастомных.
Справа пример, скриншот того, как он это показывает. Это маленький компонент, у него несложное дерево хуков, но бывают деревья и посложнее. Специальный необходимый для этого хук useDebugValue нужен, чтобы в кастомных хуках можно было добавить какую-нибудь подпись в этот Dev Tool. В данном случае у нас есть хук useConstants, и мы внутри него можем определить какую-то метку, loaded, которая определяет, загрузились ли с сервера константы, например словари.
В заключение — некоторые выводы. Прежде всего, хуки дают довольно большие возможности. С большими возможностями приходит ответственность, и я призываю вас использовать их с осторожностью. Нужно довольно хорошо понимать, как они работают, особенно то, как они мемоизируют значения. Потому что без мемоизации никуда, мы будем оптимизировать, и тут можно очень сильно себе выстрелить в ногу — мы неправильно что-то замемоизируем и оно будет хранить неактуальные значения, либо наоборот — ререндериться слишком часто.
Очень важная рекомендация. У Facebook есть специальный плагин для ESLint, который позволяет нам избежать большинства перечисленных проблем. Он отслеживает правильное использование хуков и, прежде всего, помогает. Он даже предлагает варианты, как прописать dependencies для хуков. Он их сам может прописать, и это очень удобно, всегда следует обращать внимание, на что там линтер ругается.
Так как мы работаем с кучей функций, мемоизируем их, то можем столкнуться с ситуацией, когда какое-то значение замкнулось внутри, особенно на нижних уровнях иерархии. И здесь надо четко понимать, что такое замыкание, как они работают, как функции обрабатывают свободные переменные и т. п. Без этого знания хуки лучше не использовать, потому что будет по-настоящему сложно отладить какую-то проблему.
И самое последнее — в подходе к написанию компонентов с хуками, наверное, заложена какая-то философия. Там явно чувствуется, что люди продумывали парадигму, и ее надо постичь. А это приходит только с опытом, поэтому надо пробовать, делать какие-то выводы и постепенно к ним привыкать. Возможно, в дальнейшем можно познать дзен и писать уже суперэффективный классный код на хуках. Но пока не все так просто. Полезные ссылки:
— Хуки — это просто еще один способ описывать логику ваших компонентов. Он позволяет добавить к функциональным компонентам некоторые возможности, ранее присущие только компонентам на классах.
Прежде всего это поддержка внутреннего состояния, затем — поддержка побочных эффектов. Например — сетевых запросов или запросов к WebSocket: подписка, отписка от каких-то каналов. Или, возможно, речь о запросах к каким-то другим асинхронным или синхронным API браузера. Также хуки дают нам доступ к жизненному циклу компонента, к его началу жизни, то есть монтированию, к обновлению его пропсов и к его смерти.
Наверное, проще всего проиллюстрировать в сравнении. Перед вами здесь самый простой код, который только может быть с компонентом на классах. Компонент что-то меняет. Это обычный счетчик, который можно увеличивать или уменьшать, всего одно поле в state. В общем, я думаю, что если вы знакомы с React, код для вас совершенно очевиден.
Подобный компонент, выполняющий абсолютно ту же самую функцию, но написанный на хуках, выглядит гораздо компактнее. По моим подсчетам, в среднем при портировании с компонентов на классах в компоненты на хуках код уменьшается примерно в полтора раза, и это радует.
Пару слов о том, как работают хуки. Хук — глобальная функция, которая объявлена внутри React и вызывается при каждом рендере компонента. React отслеживает вызовы этих функций и может изменить ее поведение или решить, что она должна вернуть.
На использование хуков накладываются некоторые ограничения, которые их отличают от обычных функций. В первую очередь, их нельзя использовать в компонентах на классах, просто действует такое ограничение, потому что они созданы не для них, а для функциональных компонентов. Хуки нельзя вызывать внутри внутренних функций, внутри циклов, условий. Только на первом уровне вложенности, внутри функций компонента. Это ограничение накладывает сам React, чтобы получить возможность отслеживать, какие хуки были вызваны. И он их складывает в определенном порядке у себя в мозгах. Потом, если этот порядок внезапно изменится или какие-то пропадут, возможны сложные, трудноуловимые, трудноотлаживаемые ошибки.
Но если у вас достаточно сложная логика и хотелось бы использовать, например, хуки внутри хуков, то, скорее всего, это признак, что вам стоит вынести хук. Допустим, вынести несколько связанных между собой хуков в отдельный кастомный хук. И внутри него можно использовать другие кастомные хуки, тем самым выстраивая иерархию хуков, выделяя туда общую логику.
Хуки дают некоторые преимущества по сравнению с классами. Прежде всего, как следует из предыдущего, используя кастомные хуки, можно шарить логику гораздо проще. Раньше, используя подход с Higher Order Components, мы выкладывали туда какую-то шаренную логику, и она являлась оберткой над компонентом. Теперь мы эту логику закладываем внутрь хуков. Тем самым дерево компонентов уменьшается: уменьшается его вложенность, и для React становится проще отслеживать изменения компоненты, пересчитывать дерево, пересчитывать виртуальный DOM и т. д. Тем самым решается проблема так называемого wrapper-hell. Тем, кто работает с Redux, я думаю, это хорошо знакомо.
Код, написанный с использованием хуков, гораздо легче поддается минимизации современными минимизаторами типа Terser или старым UglifyJS. Дело в том, что нам не нужно сохранять имена методов, не нужно думать о прототипах. После транспиляции, если target стоит ES3 или ES5, у нас обычно появляется куча прототипов, которые патчатся. Здесь всего этого не нужно делать, поэтому проще минимизировать. И, как следствие неиспользования классов, нам не нужно думать про this. Для новичков это часто большая проблема и, наверное, одна из главных причин багов: мы забываем про то, что this может быть window, что надо забиндить метод, например, в конструкторе или каким-то еще способом.
Также использование хуков позволяет выделить логику, управляющую каким-то одним побочным эффектом. Раньше эту логику, особенно, когда у нас несколько побочных эффектов у компонента, приходилось разбивать на разные методы жизненного цикла у компонента. И, так как появились хуки минимизации, появился React.memo, теперь функциональные компоненты поддаются мемоизации, то есть у нас этот компонент не будет пересоздан или обновлен, если его пропсы не изменились. Раньше такого нельзя было делать, теперь можно. Все функциональные компоненты можно обернуть в memo. Также внутри появился хук useMemo, который мы можем использовать для того, чтобы вычислять какие-то тяжелые значения, или инстанцировать какие-то служебные классы только единожды.
Доклад будет неполным, если я не расскажу о каких-то базовых хуках. Прежде всего, это хуки управления состоянием.
В первую очередь — useState.
Пример похож на тот, что в начале доклада. useState — это функция, которая принимает начальное значение, и возвращает tuple из текущего значения и функции для изменения этого значения. Вся магия обслуживается React внутри. Мы же можем просто либо прочитать это значение, либо его изменить.
В отличие от классов мы можем использовать столько объектов стейта, сколько нам нужно, разбивает стейт на логические куски, чтобы их не смешивать в одном объекте, как в классах. И эти куски будут друг от друга совершенно изолированы: их можно менять независимо друг от друга. Результат, например, этого кода: мы меняем две переменные, вычисляем результат и выводим кнопочки, которые позволяют нам изменить первую переменную туда-сюда, и вторую переменную тоже туда-сюда. Запомните этот пример, потому что дальше мы будем делать похожую штуку, но, гораздо более сложную.
Есть такой useState на стероидах для любителей Redux. Он позволяет более консистентно изменять стейт с помощью редьюсера. Я думаю, что те, кто знаком с Redux, можно даже не объяснять, для тех, кто незнаком, расскажу.
Редьюсер — это функция, которая принимает стейт, и некий объект, называемый обычно action, который описывает то, как этот стейт должен поменяться. Точнее, он передает какие-то параметры, а внутри reducer уже решает, в зависимости от их параметров, как стейт поменяется, и в итоге, должен вернуться новый стейт, обновленный.
Примерно таким образом он используется в коде компонента. У нас хук useReducer, он принимает функцию reducer, и вторым параметром начальное значение стейта. Возвращает, так же, как и useState, текущий стейт, и функцию для его изменения — dispatch. Если передать в dispatch объект-action, мы вызовем изменение стейта.
Очень важный хук useEffect. Он позволяет добавлять побочные эффекты для компонента, давая альтернативу жизненному циклу. В данном примере мы используем простой способ с useEffect: это просто запрос каких-то данных с сервера, с API, например, и вывод этих данных на страничке.
У useEffect есть продвинутый режим, это когда функция, переданная в useEffect, возвращает какую-то другую функцию, то эта функция будет вызвана при следующем цикле, когда этот useEffect будет применен.
Забыл упомянуть, useEffect вызывается асинхронно, сразу после того, как применится изменение к DOM. То есть он гарантирует, что он будет выполнен после рендера компонента, и может привести к следующему рендеру, если какие-то значения изменятся.
Здесь мы с вами впервые встречаемся с таким понятием, как dependencies. Некоторые хуки — useEffect, useCallback, useMemo — вторым аргументом принимают массив значений, которые позволят сказать, что отслеживать. Изменения в этом массиве приводят к каким-то эффектам. Например, здесь гипотетически у нас какой-то компонент выбора автора из какого-то списка. И табличка с книжками этого автора. И при изменении автора, будет вызван useEffect. При изменении этого authorId будет вызван запрос, и загрузятся книжки.
Также вскользь упомяну такие хуки, как useRef, это альтернатива React.createRef, что-то похожее на useState, но изменения в ref не приводят к рендеру. Иногда удобно для каких-то хаков. useImperativeHandle позволяет нам объявить у компонента определенные «публичные методы». Если использовать useRef в родительском компоненте, то он может эти методы дернуть. Если честно, один раз пробовал в учебных целях, на практике не пригодилось. useContext — прямо хорошая штука, позволяет взять текущее значение из контекста, если провайдер где-то выше по уровню иерархии определил это значение.
Существует один способ оптимизации React-приложений на хуках, это мемоизация. Мемоизацию можно разделить на внутреннюю и внешнюю. Сначала о внешней.
Это React.memo, практически альтернатива классу React.PureComponent, который отслеживал изменение в пропсах и менял компоненты только тогда, когда изменились пропсы или стейт.
Здесь же похожая штука, правда, без стейта. Она тоже отслеживает изменения в пропсах, и если пропсы изменились, происходит ререндер. Если пропсы не менялись, компонент не обновляется, и мы на этом экономим.
Внутренние способы оптимизации. Прежде всего, это достаточно низкоуровневая штука — useMemo, редко используется. Она позволяет вычислять какое-то значение, и перевычислять его только, если указанные в зависимостях значения изменились.
Есть частный случай useMemo для функции, называется useCallback. Она, прежде всего, применяется для того, чтобы замемоизировать значение функциий-обработчиков событий, которые будут переданы в дочерние компоненты для того, чтобы эти дочерние компоненты могли не перерендериваться лишний раз. Используется она просто. Мы описываем некую функцию, оборачиваем ее в useCallback, и указываем, от каких переменных она зависит.
У многих возникает вопрос, а надо ли нам это? Надо ли нам хуки? Переезжаем или остаемся как раньше? Тут однозначного ответа нет, все зависит от предпочтений. В первую очередь, если вы прямо жестко завязаны на объектно-ориентированное программирование, если ваши компоненты, вы привыкли, что это класс, у них есть методы, которые можно дернуть, то, наверное, вам может показаться эта штука лишней. В принципе, мне так и показалось, когда я впервые услышал о хуках, что слишком как-то переусложнено, какая-то магия добавляется, и непонятно зачем.
Для любителей функциональщины, это, скажем, прямо must have, потому что хуки — это функции, и к ним применимы приемы функционального программирования. К примеру, их можно комбинировать или вообще делать что угодно, используя, например, библиотеки, типа Ramda, и тому подобных.
Так как мы избавились от классов, нам теперь нет необходимости привязывать контекст this к методам. Если использовать эти методы в качестве колбеков. Обычно, это была проблема, потому что нужно было не забыть забиндить их в конструкторе, либо использовать неофициальное расширение синтаксиса языка, такие arrow-functions в качестве property. Довольно распространенная практика. Я же использовал свой декоратор, что тоже, в принципе, экспериментально, на методы.
Есть разница в том, как устроен жизненный цикл, как им управлять. Хуки практически все действия с жизненным циклом связывают с хуком useEffect, который позволяет подписаться, как на рождение, так и на обновление компоненты, так и на его смерть. В классах же для этого приходилось переопределять несколько методов, типа componentDidMount, componentDidUpdate, и componentWillUnmount. Также метод shouldComponentUpdate теперь можно заменить на React.memo.
Есть достаточно небольшая разница в том, как обрабатывается состояние. Во-первых, у классов у нас один объект состояния. Мы должны были запихать туда все, что угодно. В хуках же мы можем разбить логическое состояние на какие-то куски, которыми нам удобно было бы оперировать раздельно.
setState() у компонентов на классах позволял указывать патч стейта, тем самым изменяя одно или несколько полей стейта. В хуках же мы должны менять весь стейт целиком, и это даже хорошо, потому что модно, что ли, использовать всякие Immutable-штуки и никогда не рассчитывать на то, что у нас объекты могут мутировать. Они всегда у нас новые.
Основная фишка классов, которой у хуков нет: мы могли подписаться на изменения состояния. То есть мы меняем состояние, и тут же подписываемся на его изменения, императивным способом обрабатывая что-то сразу после того, как изменения будут применены. В хуках это сделать просто так не получится. Это нужно очень интересным способом делать, я дальше расскажу.
И коротенько про функциональный способ обновления. Он и там и там работает, это когда функции изменения состояния принимают другую функцию, которая это состояние должна не изменить, а, скорее, создать. И если в случае с классовым компонентом он может вернуть нам какой-то patch, то в хуках мы должны вернуть новое значение целиком.
В общем, вы вряд ли получите ответ, переезжать или нет. Но я советую хотя бы попробовать, хотя бы для нового кода, чтобы прочувствовать. Когда я только начал работать с хуками, то сразу же выделил несколько кастомных хуков, удобных для себя, для своего проекта. В принципе, я пытался заменить некоторые возможности, которые у меня были реализованы через Higher Order Components.
useDismounted — для тех, кто знаком с RxJS, там есть возможность массово отписаться от всех Observable в рамках одного компонента, или в рамках одной функции, передав подписку каждого Observable в специальный объект, Subject, и когда он закрывается, все подписки отменяются. Это очень удобно, если компонент сложный, если там внутри много каких-то асинхронных операций через Observable, удобно сразу разом от всех отписываться, а не от каждого отдельно.
useObservable возвращает значение из Observable, когда оно там появится новое. Похожий хук useBehaviourSubject возвращает из BehaviourSubject. Его отличие от Observable в том, что у него изначально есть какое-то значение.
Удобный кастомный хук useDebouncedValue позволяет нам организовать, например, саджест для поисковой строки, чтобы не на каждое нажатие клавиши что-то отправлять на сервер, а подождать, пока пользователь закончит вводить.
Два похожих хука. useWindowResize возвращает текущие актуальные значения для размеров окна. Следующий хук для позиции скролла — useWindowScroll. Я их использую для пересчета некоторых pop-up или модальных окон, если там какие-то сложные штуки, которые с CSS просто так не делаются.
И такой небольшой хук для реализации горячих клавиш, которые компонент, когда он присутствует на странице, он подписан на какую-то горячую клавишу. Когда он умирает, происходит автоматическая отписка.
В чем удобны вообще вот эти кастомные хуки? В том, что мы можем внутрь хука запихать отписку, и нам можно не думать о том, чтобы отписываться вручную где-то в том компоненте, где этот хук используют.
Не так давно мне кинули ссылочку на библиотеку react-use, и оказалось, что большая часть вот этих кастомных хуков там уже реализовано. И я писал велосипед. Это иногда полезно, но в дальнейшем, наверное, скорее всего, я их выкину, и использую react-use. И вам советую тоже посмотреть, если собираетесь использовать хуки.
Собственно, главная цель доклада — показать то, как неправильно писать, какие проблемы могут быть и как их избежать. Самое первое, наверное, с чем сталкивается любой, кто изучает эти хуки и пытается что-то написать, это использовать useEffect неправильно. Здесь тот код, подобный которому 100% каждый писал, если пробовал хуки. Он связан с тем, что useEffect поначалу воспринимается ментально, как альтернатива componentDidMount. Но, в отличие от componentDidMount, который вызывается единожды, useEffect вызывается при каждом рендере. И ошибка здесь в том, что он изменяет, допустим, перемену data, и при этом ее изменение приводит к ререндеру компонента, как следствие, эффект будет перезапрошен. Тем самым мы получаем бесконечную череду AJAX запросов на сервер, и компонент сам себя постоянно обновляет, обновляет, обновляет.
Исправить его очень просто. Нужно вот сюда добавить пустой массив тех зависимостей, от которых он зависит, и изменения в которых будут перезапускать эффект. Если у нас здесь указан пустой список зависимостей, то эффект, соответственно, не будет перезапускаться. Это не какой-то хак, это базовая особенность, что ли, использования useEffect.
Допустим, мы исправили. Теперь немножко усложнили. У нас есть компонент, который рендерит что-то, что нужно взять из сервера по какому-то айдишнику. В этом случае, в принципе, все работает нормально до тех пор, пока мы не изменим entityId в родителе, возможно, для вашего компонента это и не актуально.
Но скорее всего, если он изменится или появится необходимость его изменить, а у вас на странице окажется старый компонент и выяснится, что он не обновляется, то лучше добавить вот сюда entityId, в качестве зависимости, вызвав обновление, актуализацию данных.
Более сложный пример с useCallback. Здесь, на первый взгляд, все хорошо. Мы имеем некую страничку, у которой есть какой-то countdown timer, или, наоборот, timer просто, который тикает. И, например, список хостов, и сверху фильтры, которые позволяют этот список хостов фильтровать. Ну, maintenance здесь добавлен просто как для иллюстрации часто изменяющегося значения, который переводит к рирендеру.
И в данном случае, проблема в том, что изменение внутри maintenance будет приводить к ререндеру, в том числе, и объекта фильтров, потому что фильтры подписываются на onChange. Туда мы передаем функцию onChange, которая каждый раз при каждом рендере будет новая. Тем самым, если HostFilters какой-то сложный, например, в нашем случае это куча dropdown, в которые внутри данные загружены. И если он будет перерендериваться лишний раз, то производительность может просесть. Появятся лаги, или просто опять же бесконечная череда запросов.
Здесь обязательно нужно этот onChange просто обернуть в useCallback. В нашем случае он ни от чего не зависит, поэтому в конце пустой массив указываем.
Есть еще некоторые сложности, которые я выделил. Они, возможно, больше мои. То есть эти сложности отвергает Facebook, как создатель React. Они говорят, что, типа, все хорошо, все о'кей. Но все равно некое смущение, что ли, или confusing бывает.
Что сразу бросается в глаза? Так как у нас теперь функциональный компонент — это не просто кусок рендера, а есть какая-то логика и, возможно, внутри указана куча всяких функций, то все эти функции и объекты, скорее всего, будут использованы только один раз или редко когда меняться. Но при этом инициализироваться они будут на каждый рендер.
Например, здесь выделено желтым то, что будет каждый раз при каждом рендере загружаться в память, то есть будет функция создаваться, а потом она будет не использоваться, потому что не нужна она, допустим. И получается, Garbage Collector вынужден ее очищать после каждого рендера, или когда ему захочется. И это, наверно, моя главная претензия к хукам, и я не знаю, как ее решить радикально. Есть определенные хаки, когда, допустим, сильно может помочь вынос логики в reducer, который как бы внешняя функция, и там можно большую часть логику по изменению стейта вынести. Но, все равно мы не решили принципиально.
Еще есть такая проблема, она очень нервирует что ли, или вызывает фрустрацию. Когда у нас меняется какое-то значение, например, мы делаем setValue для какого-то значения, то мы не можем в хуках подписаться на изменение этого значения напрямую, как в setState можно было раньше вторым аргументом. Нам нужно как-то декларативно подписаться через изменения зависимости у useEffect.
Этот же useEffect может вызвать какой-то другой эффект, или изменение каких-то других переменных, которые, в свою очередь, вызовет еще один useEffect. И эта цепочка взаимосвязанных useEffect очень сложно кладется в голову, ее сложно отлаживать. И зачастую мне это очень сильно напоминает ситуацию, когда в старых фреймворках, типа Backbone, приходилось подписываться: допустим, у нас есть классы моделей, классы коллекций, они бросают постоянно разные события на изменение каких-то своих кусков. И мы подписываемся на эти события, потом можем их скомбинировать, бросить какое-то еще одно событие, более глобальное. Оно уже вызовет где-то еще другое событие. И, в общем, мы получаем такое событие внутри события, которое вызывает другое событие, и это тоже очень-очень сложно отлавливать. И, вроде бы, компонент поменялся, а какую плеяду, скажем, других изменений он вызывает, не ясно. Здесь эта же проблема сохранилась.
И забавная такая, даже не претензия, а замечание. Иногда, когда мы выносим нашу логику в кастомные хуки, нам хочется эту логику переиспользовать. Но мы не можем это сделать в компонентах старых на классах, потому что они не поддерживают хуки. В моем случае это был довольно сложный компонент, куда можно было вбивать данные. Он искал, его можно было выбирать, типа, dropdown такой сложный. Он был большой, давно еще написанный на классах. Я для другого dropdown сделал pop-up, который автоматом использовал хуки useWindowScroll, useWindowResize и пересчитывал свой размерчик в зависимости от высоты экрана, очень удобно. Я хотел эту же логику использовать для этого компонента, который у меня сложный на классах, — не вышло, пришлось его переписывать.
И вот эта необходимость переписывать компоненты каждый раз, вызывает определенную сложность, что ли. Получается, мы и платим технический долг по переписыванию, но, с другой стороны, нам хочется заниматься продуктовыми фичами, а приходится переливать из пустого в порожнее. Но, наверное, это скорее плюс, потому что это дает более чистый, более единообразный код, и больше заставляет нас портировать старый код на новый.
Время еще есть, и есть небольшой раздел «Бонус», связанный с хуками. Я, как и многие в Яндексе, пишу на TypeScript и использую строгую типизацию. Есть определенная проблема в использовании редьюсеров. Она заключается в том, что reducer в Redux когда, он принимает объект action. Так вот, структура этого action может быть совершенно разной, в зависимости от типа action. И сделать так, чтобы работал автокомплит, и другие плюшки статической типизации, довольно сложно.
Я нашел следующий способ. Прежде всего, мы объявляем все перечисления со всеми типами action. Можем сделать вот так просто, тогда это будут числа просто, IncrementA это 0, потом 1, 2, и так далее. Можем для удобства отладки указать их в виде строк. Естественно, они не должны пересекаться, но, в принципе, вам компилятор не даст сделать одинаковые строки. Потом для каждого типа action мы описываем структуру этого action, расширяя его какими-то полями и обязательно указывая тип. Потом создаем UnionType “Action”, где мы перечисляем через вертикальную черту все типы данных, которые туда складываются, какими могут быть action. Это первый трюк.
Второй трюк — вычисление типа на основе значения. Например, здесь есть initialState, просто объект. И по идее, надо описать структуру этого объекта еще каким-нибудь одним интерфейсом. Но можно переложить эту задачу на TypeScript. Он умеет сам вычислять значения. Например, здесь я определяю typeState только для чтения, вычисляемый из initialState.
Дальше эти типы легко использовать в reducer. Мы передаем State, Action, и дальше происходит магия: мы используем switch по action.type. В TypeScript есть такая фича при использовании UnionType: он может вычислить допустимый тип внутри case, если эти типы отличаются значением какого-то поля, в нашем случае type. И мы как раз получаем возможность обращаться к полям этого action нужного нам типа.
Естественно, это удобно: прежде всего, это валидация по типам, подсказки автокомплита и все такое. И рефакторинг.
Как это применять? В принципе, при использовании особой разницы нет. Был предыдущий пример с небольшим калькулятором. Здесь мы делаем то же самое, только через reducer. Точнее, мы определяем action creator с типами, которые нам нужны, и потом просто отправляем их в dispatch.
Еще хочу пропагандировать использование extension официального реактовского Dev Tools. Они добавили поддержку хуков и позволяют очень классно показывать все дерево хуков. Особенно полезно для кастомных.
Справа пример, скриншот того, как он это показывает. Это маленький компонент, у него несложное дерево хуков, но бывают деревья и посложнее. Специальный необходимый для этого хук useDebugValue нужен, чтобы в кастомных хуках можно было добавить какую-нибудь подпись в этот Dev Tool. В данном случае у нас есть хук useConstants, и мы внутри него можем определить какую-то метку, loaded, которая определяет, загрузились ли с сервера константы, например словари.
В заключение — некоторые выводы. Прежде всего, хуки дают довольно большие возможности. С большими возможностями приходит ответственность, и я призываю вас использовать их с осторожностью. Нужно довольно хорошо понимать, как они работают, особенно то, как они мемоизируют значения. Потому что без мемоизации никуда, мы будем оптимизировать, и тут можно очень сильно себе выстрелить в ногу — мы неправильно что-то замемоизируем и оно будет хранить неактуальные значения, либо наоборот — ререндериться слишком часто.
Очень важная рекомендация. У Facebook есть специальный плагин для ESLint, который позволяет нам избежать большинства перечисленных проблем. Он отслеживает правильное использование хуков и, прежде всего, помогает. Он даже предлагает варианты, как прописать dependencies для хуков. Он их сам может прописать, и это очень удобно, всегда следует обращать внимание, на что там линтер ругается.
Так как мы работаем с кучей функций, мемоизируем их, то можем столкнуться с ситуацией, когда какое-то значение замкнулось внутри, особенно на нижних уровнях иерархии. И здесь надо четко понимать, что такое замыкание, как они работают, как функции обрабатывают свободные переменные и т. п. Без этого знания хуки лучше не использовать, потому что будет по-настоящему сложно отладить какую-то проблему.
И самое последнее — в подходе к написанию компонентов с хуками, наверное, заложена какая-то философия. Там явно чувствуется, что люди продумывали парадигму, и ее надо постичь. А это приходит только с опытом, поэтому надо пробовать, делать какие-то выводы и постепенно к ним привыкать. Возможно, в дальнейшем можно познать дзен и писать уже суперэффективный классный код на хуках. Но пока не все так просто. Полезные ссылки:
- «React hooks — победа или поражение?» Cтатья очень перекликается с моим докладом. Я про нее узнал уже после того, как доклад был готов, но добавил просто потому, что там мысли совпадают с моими, а это всегда приятно.
- Полное руководство по useEffect. Переводная статья, тоже на Хабре, там прямо досконально по полочкам разложено, как это работает, как его использовать, как не ошибаться и т. д.
- Статья «useReducer vs useState in React» объясняет, когда нужно использовать useReducer, а когда можно обойтись простым useState. Спойлер: если у вас сложный стейт, особенно поля, зависимые друг от друга, то лучше использовать useReducer. Если у вас два-три значения, то обычного useState хватит с головой.
- Сайт React Hooks CheatSheets c множеством интерактивных примеров для изучения хуков.
- Библиотеки кастомных хуков. Usehooks.com — это просто сайт, блог. Там люди выкладывают свои хуки. И я уже упоминал react-use — библиотечку, содержащую полезные для большинства проектов кастомные хуки, которых еще нет из коробки.