В данной статье я расскажу и покажу на примерах, как реализовать глобальное хранилище состояния в React или Next, и зачем вообще оно нужно.

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

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

(Сразу скажу, что это не реклама, т.к. я не являюсь автором useGlobalHook, и пишу этот пост исключительно из за того, что он мне нравится. Хочу научить этому способу нескольких своих учеников, а заодно делюсь и с вами).

Зачем вообще нужно глобальное хранилище состояния?

Я не мог отказаться от этого абзаца, так как кто-то может не знать ответ на этот вопрос.

Если вы начинающий разработчик использующий React, то скорее всего уже сталкивались с хуком useState. На всякий случай разберём его использование на простом примере:

Пример служит лишь для демонстрации работы хука useState
Пример служит лишь для демонстрации работы хука useState

При нажатии на кнопку с именем, блок с H1 будет перерисован, и будет отображаться выбранное имя. При этом перезагрузки страницы не произойдёт. Это то, для чего нам нужен useState.

Когда мы создаём переменную useState, мы объявляем две переменные - в первой (userName) будет храниться значение, а вторая (setUserName) служит функцией для записи значения в первую переменную. Когда мы меняем значение переменной, перерисовывается та часть проекта, в которой используется эта переменная без перезагрузки страницы.

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

Например, мы хотим хранить имя пользователя, и отображать его в разных компонентах (в шапке приложения, в модальных окнах, и тд), а так же еще некоторые данные, которые используются в разных местах проекта.

Как же быть? Поскольку пример простой, может показаться хорошей идеей пробрасывание через props, но в реальных проектах вы можете столкнуться с тем, что нужно будет пробрасывать более чем через один компонент, что создаст очень сложную систему.

Должен быть более простой способ! Из всех существующих мне больше нравится useGlobalHook, поэтому перейдём сразу к нему.

Реализация глобального хранилища

Первым делом установим npm-пакет use-global-hook

npm i use-global-hook

Или если вы как я используете yarn, то

yarn add use-global-hook

Далее создадим определённую файловую структуру:

В папке src создадим папку store

В папке store создадим файл index.js со следующим содержанием:

import GlobalHook from "use-global-hook";
import * as actions from "./actions";

const initialState = {
  
  user_name: "",
  user_role: "user",
  // здесь перечислите все переменные которые должны быть глобальными
  // можете задать им значния по умолчанию

};

const useGlobal = GlobalHook( initialState, actions );

export default useGlobal;

В переменной initialState мы перечисляем абсолютно все переменные, которые мы хотим чтобы были доступны из любой части проекта.

А что же там за import * from "./actions"?

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

Создайте эту папку в папке src/store, и создайте в ней файл index.js.

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

В /src/actions/index.js мы будем импортировать функции из этих файлов.

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

В какую логическую группу мы можем определить эту функцию? Правильно, система авторизации. Поэтому мы создадим в папке actions файл sign.js

Прежде чем писать нашу функцию, мы должны импортировать все функции из файла sign.js в файл src/actions/index.js, а также экспортировать их из этого файла, чтобы в дальнейшем использовать их через useGlobalHook. Вот как мы это делаем:

Напишите в файл src/actions/index.js следующее:

import * as sign from './sign';

export {

  sign,
  
}

Хорошо, давайте теперь напишем нашу функцию авторизации, где будет получение данных с бэкенда и изменения глобальных стейтов. Поскольку это функция авторизации (по английски signin) в файле src/actions/sign.js создадим функцию In, и экспортируем её:

export async function In( store, email, pass ) {

  try {

    // тут должен быть запрос к серверу, который запишет ответ в переменную data
    // но чтобы не усложнять пример я просто имитирую ответ сервера
    // создав переменную data:

    const data = {
      name: "Андрей",
      role: "admin"
    }

    // типа мы получили ответ сервера с нужными данными
    // теперь мы должны записать их в глобальный стейт

    // делаем мы это вот так:

    store.setState({

      user_name: data.name, // задаём значение user_name
      user_role: data.role, // задаём значение user_role

    });

    // тут может быть редирект на другую страницу

  } catch ( err ) {

    console.error( err );
    // тут обработка ошибок. я не стал усложнять пример
    // но в реальном проекте не игнорируйте этот блок
    // (если не знаете, погуглите try catch )

  }
  
}

Обратите внимание на важную вещь! Когда мы объявляем экшены useGlobalHook (т.е. создаём такие функции), мы всегда в них принимаем первым аргументом "store". Через store мы можем взаимодействовать с глобальным хранилищем внутри этой функции следующими способами:

Изменять глобальные переменные:

store.setState({
  глобальнаяпеременна: новоезначение,
})

Получать значение глобальной переменной (внутри этой функции):

store.state.названиепеременной

Вызывать другую функцию изменения глобального значения (другой экшен):

store.actions.названиеФункции()

Таким образом мы взаимодействуем с глобальным хранилищем внутри функций-экшенов.

Вне подобных функций мы взаимодействуем с ним другим способом, о котором я сейчас как раз таки и расскажу.

Давайте создадим тестовую страницу, на которой мы будем имитировать авторизацию. Смотрите, нам нужно сначала ввести email и пароль, а их следует записать в переменную. Эти данные нам не нужны глобально, поэтому мы будем использовать обычный useState.

Это важно - не нужно запихивать в глобальное хранилище то, что используется только лишь на одной странице, или только лишь в одном компоненте!

import { useState } from "react";

const Auth = () => {

  const [ email, setEmail ] = useState("");
  const [ password, setPassword ] = useState("");

  return (

    <div>

      <input

        type = "text"
        value = { email }
        onChange = { ( event ) => { setEmail( event.target.value ) }}

      />

      <input

        type = "password"
        value = { password }
        onChange = { ( event ) => { setPassword( event.target.value ) }}

      />

      <button>Авторизоваться</button>

    </div>

  );

}

export default Auth;

Каркас готов, теперь мы должны в нём использовать наше глобальное хранилище. Давайте для простоты примера сделаем так: если наша глобальная переменная user_name пуста, мы будем показывать форму авторизации. Если же в ней что-то есть, мы будем показывать надпись "%Имя%, вы успешно авторизовались!".

Для доступа к глобальным стейтам и экшенам мы делаем следующее. В любом компоненте, где они нам нужны, мы пишем так:

import useGlobal from 'путь/к/папке/store';

const MyComponent = () => {

  const [ globalState, globalActions ] = useGlobal();

  return (
    ///...
  );

}

Смотрите, в globalState у нас находятся наши глобальные переменные, а в globalActions наши глобальные функции-экшены. Давайте для начала в нашей странице авторизации создадим условие, о котором я писал выше - если имя пользователя пустое, показываем авторизацию, если нет, то сообщение в которой задействуется эта переменная.

Для удобства я деструктурирую из globalState те свойства, которые нам нужны. В данном примере мы будем использовать переменную user_name, давайте её деструктурируем:

const [ globalState, globalActions ] = useGlobal();

const { user_name } = globalState;

Всё, мы можем получать значение из глобальной переменной user_name внутри этой страницы! (Чтобы получить значение на другой странице или в другом компоненте, мы делаем то же самое). Давайте напишем условие о котором я писал выше. Теперь наша страница авторизации выглядит так:

import { useState } from "react";
import useGlobal from 'путь/../store'; // тут должен быть п

const Auth = () => {

  const [ globalState, globalActions ] = useGlobal();

  const { user_name } = globalState;

  const [ email, setEmail ] = useState("");
  const [ password, setPassword ] = useState("");

  return (

    user_name === "" 
    
      ? <div>

          <input

            type = "text"
            value = { email }
            onChange = { ( event ) => { setEmail( event.target.value ) }}

          />

          <input

            type = "password"
            value = { password }
            onChange = { ( event ) => { setPassword( event.target.value ) }}

          />

          <button>Авторизоваться</button>

        </div>

      : <div>

          { user_name }, вы успешно авторизовались!

        </div>

  );

}

export default Auth;

Мы научились использовать значения глобальных переменных, а теперь давайте научимся вызывать функции-экшены, которые изменяют эти значения. Нам нужно при клике на кнопку "авторизоваться" вызвать глобальную функцию In, которая находится в файле sign. Давайте для удобства деструктурируем функции файла sign из globalActions в переменную sign:

const [ globalState, globalActions ] = useGlobal();

const { user_name } = globalState;

const { sign } = globalActions;

Помните как мы импортировали функции из файла sign в store/actions/index.js и экспортировали их оттуда? Поэтому их теперь можно вызывать из globalActions.sign.

Когда у нас будет несколько логических групп (файлов-экшенов), они будут вызываться также - мы создаём отдельный файл в store/actions/, пишем там все функции этой логической группы, далее импортируем их в store/actions/index.js и экспортируем оттуда как выше я объяснял. И у нас получается конструкция вызова следующая:

globalActions.название_Файла_Логической_Группы.название_Функции_Из_Этого_Файла()

Чтобы каждый раз не прописывать перед вызовом функции "globalActions.", мы будем деструктурировать группы функций, как я описал выше.

Теперь нам нужно по клику на кнопку вызвать функцию In и передать в неё email и password:

<button onClick = { () => sign.In( email, password ) } >
            
  Авторизоваться
  
</button>

Обратите внимание на важный аспект! Когда мы объявляли функцию In, первым аргументом в ней был store, а email уже вторым, а password третьим:

export async function In( store, email, pass ) {

А когда мы её вызываем, мы указываем email первым аргументом!

Это особенность функций-экшенов useGlobalHook - когда мы их вызываем, мы не передаём первый аргумент store, но в функции всегда первым аргументом мы ожидаем store, чтобы мочь взаимодействовать внутри функции с глобальным хранилищем. Запомните, это очень важно!

Итак, наш тестовый пример готов. Вот итоговый код страницы авторизации:

import { useState } from "react";
import useGlobal from 'путь/к/папке/store';

const Auth = () => {

  const [ globalState, globalActions ] = useGlobal();

  const { user_name } = globalState;
  const { sign } = globalActions;

  const [ email, setEmail ] = useState("");
  const [ password, setPassword ] = useState("");

  return (

    user_name === "" 
    
      ? <div>

          <input

            type = "text"
            value = { email }
            onChange = { ( event ) => { setEmail( event.target.value ) }}

          />

          <input

            type = "password"
            value = { password }
            onChange = { ( event ) => { setPassword( event.target.value ) }}

          />

          <button onClick = { () => sign.In( email, password ) } >
            
            Авторизоваться
            
          </button>

        </div>

      : <div>

          { user_name }, вы успешно авторизовались!

        </div>

  );

}

export default Auth;

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

Функция быстрого изменения глобальной переменной

Не редко бывает, что нужен какой то быстрый способ изменить любую глобальную переменную, чтобы не писать функцию для изменения значения именно этой переменной. Для этого я придумал универсальную функцию изменения любой глобальной переменной. Давайте её напишем, и научимся использовать! В файл store/actions/index.js добавьте функцию changeStates, теперь этот файл выглядит вот так:

import * as sign from './sign';

async function changeStates( store, states ) {

  store.setState( states );

}

export {

  changeStates,
  sign,
  
}

Теперь в любом React-компоненте мы можем изменять значение одной или нескольких глобальных переменных вот таким образом:

import useGlobal from 'путь/к/папке/store';

const SomeComponent = () => {

  const [ globalState, globalActions ] = useGlobal();

  const { user_name, user_role } = globalState;
  const { changeState } = globalActions; // деструктурируем универсальную ф-цию

  function Test() {

    // вызывая её, указываем переменные
    // которые хотим изменить, и их новые значения

    changeState({

      user_name: "Андрей",
      user_role: "admin"

    });

  }

  return (

    <div>

      <h1>{ user_name }</h1>
      <p>{ user_role }</p>
      <button onClick = { Test }>Проверим?</button>

    </div>

  )

}

export default SomeComponent;

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

Я специально не отвлекался на лишнее, и показал только то, что необходимо для того чтобы понять, как использовать useGlobalHook. Теперь этот инструмент в ваших руках.

Я использовал его в многих серьёзных проектах, и если передо мной встаёт выбор какой менеждер глобального хранилища использовать - я всегда выбираю именно useGlobalHook, он удобен, и хорошо зарекомендовал себя в моих глазах.

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

Я не хочу разводить холивар среди любителей других менеджеров состояния на тему "какой круче". Просто попробуйте использовать useGlobalHook, и сравните ощущения.

Спасибо за внимание!

И успехов в разработке ваших проектов.

Комментарии (3)


  1. genikineg
    26.08.2023 08:00

    Recoil


  1. markelov69
    26.08.2023 08:00

    Мда, какой только херней не занимаются лишь бы просто не использовать MobX. Куда катиться мир...


    1. Syos
      26.08.2023 08:00

      Интересно, все адепты церкви MobX безграмотные, или только этот