Как создать глобальное управление состоянием в приложениях React без побочных зависимостей и ненужного ререндеринга
Прежде всего, я хотел бы выразить особую благодарность Джеку Херрингтону за то, что он вдохновил меня на эту тему.
React добился значительных успехов в инструментах, которые помогают нам создавать глобальное управление состоянием. React вывел нас на новый уровень, а огромная база шаблонов Redux и декораторы Mobx канули в лету. Создание собственного глобального управления состоянием без побочных зависимостей — непростая задача.
Сегодня мы узнаем о хуке useSyncExternalStore
и доработаем его, чтобы создать управление состоянием без ненужного рендеринга наших компонентов.
Структура приложения
Прежде чем приступить к коду, давайте проверим структуру нашего тестового приложения. Каждый компонент будет выполнять итерации с глобальным хранилищем. Нам нужна эта структура, чтобы знать, какой компонент будет перерендерен при манипуляциях с хранилищем.
Создать компоненты
Чтобы упростить процесс разработки, я буду использовать https://vitejs.dev/ и выберу React с JavaScript. Вот шаги, чтобы начать работу:
- Беги
yarn create vite
- Выберите название проекта, фреймворк:
React
и языковой вариант:javascript
. - Добавьте следующее:
cd project_name
иyarn
- Удалите
<React.StrictMode>
вmain.jsx
, чтобы избежать повторного рендеринга
Теперь давайте создадим компоненты в src/components
. Должно быть четыре файла со следующими именами: Header.jsx
, Body.jsx
, Shares.jsx
и Footer.jsx
.
Создать магазин
В папке src
создайте папку store
и поместите в нее следующие два файла: initialState.js
и index.js
.
Для initialState.js
я добавил несколько вложенных объектов и значений, которые будут обновляться в нашем приложении. Вот как выглядит код:
Прежде чем создавать функцию, давайте разберемся, как должен работать магазин и чего нам ожидать. Подобно Redux, мы можем использовать хуки useState
или useContexts
с useReducer
и применять их во всем приложении. Давайте проверим реализацию с useState
, как показано ниже:
Как видите, мы будем повторно использовать useState
во всем приложении. Никакой магии; это простая реализация, которая уточняет, какой компонент будет перерисовываться после манипулирования хранилищем. Давайте обновим наш App.jsx
на store.useStore()
:
И пусть Footer.jsx
получает доступ к текущему состоянию с помощью следующего кода:
- Запустите приложение с помощью
yarn dev
и нажмите кнопкуUpdate Shares From App
в открытой консоли.
Вы увидите, что все наши компоненты обновлены. В Footer
мы будем читать status
из немодифицированного хранилища, и это всегда будет возвращать active
.
Но проблема в том, что мы не обновили никаких значений в Footer
, потому что получили обновленный объект, который перерендерил компонент. Чтобы избежать повторного рендеринга, мы создадим селектор и будем читать хранилище из хука useSelector
. Вот как это сделать:
const status = useSelector((state) => state.status);
Хук будет использовать селектор функций для получения текущего состояния из нашего хранилища.
Обновить компоненты
Теперь давайте создадим остальные компоненты. В каждом компоненте мы добавим console.log
к имени этого компонента. Альтернативное решение — использовать Google Chrome с инструментами разработчика React. Вот как это выглядит:
Теперь Footer.jsx
будет использовать useSelector
из созданного магазина.
Для Header
мы будем использовать только setState
из магазина.
А для Body
мы будем использовать обе функции для обновления и чтения хранилища.
И последний раздел, Shares
, будет использовать хранилище для чтения данных.
Наконец, чтобы обернуть их все в одно приложение, давайте поместим компоненты в App.jsx
. Чтобы также проверить визуализацию компонента App
, мы будем использовать функцию setState
.
Чтобы выделить наши компоненты, давайте используем App.css
для добавления границ.
header, footer, .body, .shares { border: 1px solid #2c2c2c; }
Магазин и слушатели
Чтобы предотвратить ненужную перерисовку, нам нужно создать функцию useSelector
. Это улучшит реализацию createStore
с subscribe
. Subscribe
уведомляет React об изменениях в магазине. Внутри createStore.js
давайте создадим функцию subscribe
с listeners
.
С помощью этой техники мы можем подписаться на наш магазин и уведомлять React об изменениях. Как видите, эта функция также вернет listeners.delete
и позволит нам отписаться. Эта техника пришла из модели издатель-подписчик, которая позволяет вам подписываться и отписываться от изменений. Чтобы получать уведомления об изменениях, мы должны создать еще одну функцию, setState
.
Слушатель всегда будет получать текущее состояние и устанавливать его на listeners
.
И последняя часть, функция createStore
, использует хук useSelector
и позволяет нам получать все изменения из нашего хранилища.
Но в этом случае мы не сможем получать обновленные данные, потому что мы не подписаны на наши изменения от государства. Чтобы это исправить, мы должны применить функцию subscribe
к хуку useSyncExternalStore
из React. Этот хук принимает три аргумента: subscribe
, getSnapshot
и getServerSnapshot
для рендеринга на стороне сервера.
Функция subscribe
зарегистрирует обратный вызов, чтобы уведомить нас об изменениях в магазине. А объединение () => selector(state)
и getSnapshot
вернет текущее состояние нашего магазина. В этом случае мы какое-то время не будем использовать рендеринг на стороне сервера.
Теперь давайте запустим наш сервер yarn dev
и проверим, как будут перерисовываться компоненты. Вы увидите что-то вроде этого:
При нажатии на кнопку Update Shares From App
данные магазина обновятся. Эти данные используются только в Shares.jsx
, и это единственный компонент, который необходимо перерендерить, поскольку другие компоненты не получали обновлений.
Теперь нажмите на Update Name And Age from Header
, и вы увидите, что обновления происходят только в Body.jsx
. И если вы нажмете еще раз, ничего не будет перерисовано, потому что данные те же. Это абсолютно нормально.
Как насчет рендеринга на стороне сервера
Чтобы синхронизировать данные на стороне сервера и хранить их, нам нужно улучшить функцию createStore
. Чтобы проверить это, я предлагаю вам создать приложение Next JS
и применить созданные нами компоненты к представлению index
. Пока вы это делаете, добавьте функцию getServerSideProps
, чтобы внести дополнительные изменения в данные магазина.
Чтобы применить новые данные магазина из нашего представления, мы должны инициализировать наш магазин данными сервера из props
.
Функция init
должна получить новое состояние и применить его к нашему текущему состоянию. Вот как это выглядит:
Назначение произойдет только один раз для view
.
Заключение
Это увлекательно! С помощью одной функции мы решили глобальную проблему управления состоянием без какого-либо шаблонного кода или ненужного повторного рендеринга. Хук useSyncExternalStore
помогает нам синхронизировать наш магазин с состоянием нашего приложения React. Всего одна функция может связать значения нашего глобального хранилища со всем приложением.
Ресурсы
Репозиторий GitHub: https://github.com/antonkalik/global-store
Want to Connect? I'll be glad to keep in touch through Twitter. It's always a pleasure to receive suggestions and comments related to the topic. Feel free to ask any questions. Thank you!