Создание Redux-подобного глобального хранилища, используя React Hooks

от автора

Привет, Хабр! Представляю вашему вниманию перевод статьи "Build a Redux-like Global Store Using React Hooks" автора Ramsay.

Давайте представим, что я написал интересное предисловие к этой статье и теперь мы сразу можем перейти к по-настоящему интересным вещам. Если говорить вкратце, то мы будем
использовать useReducer и useContext для создания пользовательского хука React, который обеспечит доступ к глобальному хранилищу, похожему на Redux.

Я не в коем случае не предполагаю, что это решение является полным эквивалентом Redux, потому-что я уверен, что это не так. Говоря "Redux-подобное", я имею ввиду то,
что вы будете обновлять хранилище, используя dispatch и actions, которые будут проводить мутацию над состоянием хранилища и возвращать новую копию мутировавшего состояния.
Если вы никогда не пользовались Redux, просто притворитесь, что не читали этот абзац.

Хуки

Давайте начнем с создания контекста(далее Context) который будет содержать наше состояние(далее state) и функцию диспетчеризации(далее dispatch). Мы так же создадим функцию useStore, которая и будет вести себя, как наш хук.

// store/useStore.js  import React, { createContext, useReducer, useContext } from "react";  // пока оставим это пустым const initialState = {}  const StoreContext = createContext(initialState);  // useStore будет использоваться в React компонентах для извлечения и мутации состояния export const useStore = store => {   const { state, dispatch } = useContext(StoreContext);   return { state, dispatch }; }; 

Так как все хранится внутри React Context, нужно создать Provider, который даст
нам объект state и функцию dispatch. Provider находится там, где мы используем useReducer.

// store/useStore.js  ... const StoreContext = createContext(initialState);  export const StoreProvider = ({ children }) => {   const [state, dispatch] = useReducer(reducer, initialState);   return (     <StoreContext.Provider value={{ state, dispatch }}>       {children}     </StoreContext.Provider>   ); }; ...

Мы используем useReducer, что бы получить state и dispatch. Собственно, это как раз то, что и делает useReducer. Далее мы передаем state и dispatch  в Provider.
Теперь мы можем обернуть любой компонент React с помощью <Provider/> и этот компонент сможет использовать useStore, что бы взаимодействовать с хранилищем.

Мы еще не создали reducer. Это будет нашим следующим шагом.

// store/useStore.js ... const StoreContext = createContext(initialState);  // это будет мапингом actions, которые будут инициировать мутации state const Actions = {};  // reducer вызывается всякий раз, когда action совершается через функцию dispatch // action.type - это строка, которая соответствует функции в Actions // мы применяем update к текущему state и возвращаем его новую копию const reducer = (state, action) => {   const act = Actions[action.type];   const update = act(state);   return { ...state, ...update }; }; ...

Я большой фанат разделения actions и state в логические группы, например: вам может быть нужно отслеживать состояние счетчика(классический пример реализации счетчика) или состояние пользователя(зашел ли пользователь в систему или его персональные предпочтения).
В каком-то компоненте вам может понадобиться доступ к обоим этим состояниям, так что идея хранить их в едином глобальном хранилище вполне имеет смысл. Мы можем разделить наши actions в логические группы, такие как userActions и countActions, что сделает управление ими намного проще.

Давайте создадим файлы countActions.js и userActions.js в папке store.

// store/countActions.js  export const countInitialState = {   count: 0 };  export const countActions = {   increment: state => ({ count: state.count + 1 }),   decrement: state => ({ count: state.count - 1 }) };

// store/userActions.js  export const userInitialState = {   user: {     loggedIn: false   } };  export const userActions = {   login: state => {     return { user: { loggedIn: true } };   },   logout: state => {     return { user: { loggedIn: false } };   } };

В обоих этих файлах мы экспортируем initialState, потому-что хотим потом объединить их в файле useStore.js в единый объект initialState.
Так же, мы экспортируем объект Actions, который предоставляет функции для мутаций состояния. Заметьте, что мы не возвращаем новый объект состояния, потому-что хотим, что бы это происходило в reducer, в файле useStore.js.

Теперь импортируем это все в useStore.js, что бы получить полную картину.

// store/useStore.js  import React, { createContext, useReducer, useContext } from "react";  import { countInitialState, countActions } from "./countActions"; import { userInitialState, userActions } from "./userActions";  // объединение начальных состояний (initial states) const initialState = {   ...countInitialState,   ...userInitialState };  const StoreContext = createContext(initialState);  // объединение actions const Actions = {   ...userActions,   ...countActions };  const reducer = (state, action) => {   const act = Actions[action.type];   const update = act(state);   return { ...state, ...update }; };  export const StoreProvider = ({ children }) => {   const [state, dispatch] = useReducer(reducer, initialState);   return (     <StoreContext.Provider value={{ state, dispatch }}>       {children}     </StoreContext.Provider>   ); };  export const useStore = store => {   const { state, dispatch } = useContext(StoreContext);   return { state, dispatch }; };

Мы это сделали! Сделайте круг почета, а когда вернетесь, мы посмотрим, как это все использовать в компоненте.

Добро пожаловать обратно! Я надеюсь, что ваш круг был действительно почетным. Давайте посмотрим на useStore в действии.

Сначала мы можем обернуть наш компонент App в <StoreProvider/>.

// App.js  import React from "react"; import ReactDOM from "react-dom"; import { StoreProvider } from "./store/useStore"; import App from "./App";  function Main() {   return (     <StoreProvider>       <App />     </StoreProvider>   ); }  const rootElement = document.getElementById("root"); ReactDOM.render(<Main />, rootElement);

Мы оборачиваем App в StoreProvider, что бы дочерний компонент имел доступ к значению из провайдера(provider). Этим значением является и state, и dispatch.

Теперь, давайте предположим, что у нас есть компонент AppHeader у которого есть кнопка login/logout.

// AppHeader.jsx  import React, {useCallback} from "react"; import { useStore } from "./store/useStore";  const AppHeader = props => {   const { state, dispatch } = useStore();   const login = useCallback(() => dispatch({ type: "login" }), [dispatch]);   const logout = useCallback(() => dispatch({ type: "logout" }), [dispatch]);    const handleClick = () => {     loggedIn ? logout() : login();   }    return (     <div>       <button onClick={handleClick}> {loggedIn ? "Logout" : "Login"}</button>       <span>{state.user.loggedIn ? "logged in" : "logged out"}</span>       <span>Counter: {state.count}</span>     </div>   ); };  export default AppHeader;

Ссылка на Code Sandbox с полной реализацией

Автор оригинала: Ramsay
Ссылка на оригинал


ссылка на оригинал статьи https://habr.com/ru/post/460083/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *