Creating a Redux-like Global Store Using React Hooks

Hello, 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 introduction to this article, and now we can go straight to the really interesting things. In short, we will
use useReducer and useContext to create a custom React hook that will provide access to a global repository similar to 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. Speaking "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 ( hereinafter Context ) that will contain our state ( hereinafter state ) and a dispatch function ( hereinafter 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 , we need to create a Provider that will give
us a state object and a dispatch function . Provider is where we use useReducer .


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

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


We have not created reducer yet . 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 am a big fan of separating actions and state into logical groups, for example: you may need to monitor the state of the counter (a classic example of the implementation of the counter) 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 perfect sense. We can split our actions into logical groups such as userActions and countActions , which makes managing them a lot 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 initialState , because we want to later combine them in the useStore.js file into a single initialState object .
We also export an Actions object that provides functions for mutations of state. Note that we are not returning a new state object, because we want this to happen in reducer , in the useStore.js file .


Now we import it all 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 (
    
      {children}
    
  );
};
export const useStore = store => {
  const { state, dispatch } = useContext(StoreContext);
  return { state, dispatch };
};

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


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


First we can wrap our App component in.


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

We wrap the App in the StoreProvider so that the child component has 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 (
    
{state.user.loggedIn ? "logged in" : "logged out"}Counter: {state.count}
); }; export default AppHeader;

Link to Code Sandbox with full implementation


Original author: Ramsay
Link to the original


Also popular now: