В данной статье я расскажу и покажу на примерах, как реализовать глобальное хранилище состояния в React или Next, и зачем вообще оно нужно.
Очень популярно сейчас реализовывать это на Redux, но на мой взгляд реализация на Redux хуже воспринимается, сложнее для новичков, и требует больше кода.
Мы же будем использовать useGlobalHook, я постоянно использую его при разработке проектов, и на мой взгляд, он очень удобен.
(Сразу скажу, что это не реклама, т.к. я не являюсь автором useGlobalHook, и пишу этот пост исключительно из за того, что он мне нравится. Хочу научить этому способу нескольких своих учеников, а заодно делюсь и с вами).
Зачем вообще нужно глобальное хранилище состояния?
Я не мог отказаться от этого абзаца, так как кто-то может не знать ответ на этот вопрос.
Если вы начинающий разработчик использующий React, то скорее всего уже сталкивались с хуком 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)
markelov69
26.08.2023 08:00Мда, какой только херней не занимаются лишь бы просто не использовать MobX. Куда катиться мир...
genikineg
Recoil