Как создать глобальное управление состоянием в приложениях 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!