📜 ⬆️ ⬇️

Create Redux-like global storage using React Hooks

Hi, Habr! I present to you the translation of the article "Build a Redux-like Global Store Using React Hooks" by Ramsay.


Let's imagine that I wrote an interesting preface to this article and now we can immediately move on to really interesting things. In short, we will
use useReducer and useContext to create a custom React hook that provides access to a global repository like Redux.


I do not in any way assume that this solution is the full equivalent of Redux, because I am sure that it is not. By saying "redux-like", I mean,
That you will update the repository using dispatch and actions , which will mutate the state of the repository and return a new copy of the mutated state.
If you have never used Redux, just pretend not to read this paragraph.


Hooks


Let's start by creating a context ( hereafter Context ) that will contain our state ( hereafter state ) and the dispatch function ( hereafter dispatch ). We will also create the useStore function, which will behave like our hook.


// 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 }; }; 

Since everything is stored inside the React Context , you need to create a Provider that will give
The state object and the dispatch function. The provider is where we use 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> ); }; ... 

We use useReducer to get state and dispatch . Actually, this is exactly what useReducer does. Next, we pass state and dispatch to Provider .
Now we can wrap any React component using <Provider /> and this component will be able to use useStore to interact with the repository.


We have not created a reducer . This will be our next step.


 // 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 }; }; ... 

I'm a big fan of separating actions and state into logical groups, for example: you may need to track the state of the counter (a classic example of the counter implementation) or the state of the user (whether the user has logged into the system or his personal preferences).
In some component, you may need access to both of these states, so the idea of ​​storing them in a single global repository makes sense. We can divide our actions into logical groups, such as userActions and countActions , which will make managing them much easier.


Let's create the countActions.js and userActions.js files in the store folder.


 // 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 } }; } }; 

In both of these files, we export the initialState , because we want to later merge them in the useStore.js file into a single initialState object.
We also export the Actions object, which provides functions for state mutations. Notice that we do not return a new state object, because we want this to happen in a reducer , in the file useStore.js .


Now we import all this into useStore.js to get the full picture.


 // 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 }; }; 

We did it! Make a lap of honor, and when you return, we will see how to use all this in a component.


Welcome back! I hope your lap was truly honorable. Let's look at useStore in action.


First, we can wrap our App component in <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); 

We wrap the App in StoreProvider so that the child component would have access to the value from the provider. This value is both state and dispatch .


Now, let's assume that we have an AppHeader component that has a login / logout button.


 // 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; 

Link to Code Sandbox with full implementation


Original author: Ramsay
Link to the original


')

Source: https://habr.com/ru/post/460083/


All Articles