Привет, Хабр! Представляю вашему вниманию перевод статьи "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/
Добавить комментарий