Implementing a global store using react context and hook patterns: Part 1

Implementing a global store using react context and hook patterns: Part 1

'Prop Drilling' through React component tree is one of the worst nightmare for developers. For those who don't know what prop drilling is - it's the process of passing down data/methods from parent to children in long hierarchy. It's not a problem when the tree is small, with one or two level. But when react component gets larger, it's troublesome to keep track of all that 'life-altering' props.

To solve prop drilling and some related problems, global state management like Redux, Mobx, Unstated, Apollo Link State etc. has become unrequitedly popular. Also for React, react-redux has become oneway destination to react developers.

But, sometimes for a simple app using those state management solution might seem a overkill. Here comes React to the rescue. Using React Context API and hook patterns we can create a redux-like store, that we can use to store data, call on dispatcher for performing actions and thus solving the problem of prop drilling without installing any third party state management systems. Voila!

This will be a two part article, in first part we are going to create a redux like store, let's call it react-store from now on. Then in next part we are going to create a simple react app, to apply that react-store practically.

Let's Get Started

To get started we need to understand how React Context works. Best way to learn about the Context is to go through the official documentation. In short, Context is a way to share data across the react-app without having to pass them explicitly (remember, prop drilling). So, we will create a context, then wrap our app with it and any state we keep on the context will be available throughout the app. Seems simple enough. Actually, we will see that it is.

Then comes, react hooks. After being included in React latest releases, the hooks has become such a buzzword, to be honest it's actually deserving. We are going to use useReducer hook (Wait, where have I heard the name reducer before? Yeah, right in redux), which to be honest is the more advanced version of useState hook. To know more about them go though the official documentation as usual.

Okay, enough chit-chat. Let's create our context:

const StoreContext = createContext();

const GlobalStore = props => {
  return <StoreContext.Provider value={{}} />;
};

We have created a context called StoreContext. And in the GlobalStore function we are using StoreContext.Provider to expose the values throughout the app. Now, let's add state to our context.:

const StoreContext = createContext();

const GlobalStore = props => {
  if (props === undefined)
    throw new Error(
      "Props Undefined. You probably mixed up betweenn default/named import"
    );
  const { load, ...rest } = props;

  const [state, dispatch] = useReducer(load.reducer, load.initialState);

  return <StoreContext.Provider value={{ state, dispatch }} {...rest} />;
};

So, we have introduced a local state for the whole context using useReducer. It returns state which have the initial state of the app that comes from prop load.initialState and dispatch that can takes actions on the state through the load.reducer. We will create a custom hook using useContext to expose the values. So, the final file GlobalStore.js will look like this:

import React, { createContext, useContext, useReducer } from "react";

const StoreContext = createContext();

const GlobalStore = props => {
  if (props === undefined)
    throw new Error(
      "Props Undefined. You probably mixed up betweenn default/named import"
    );
  const { load, ...rest } = props;

  const [state, dispatch] = useReducer(load.reducer, load.initialState);

  return <StoreContext.Provider value={{ state, dispatch }} {...rest} />;
};

export const useStore = () => useContext(StoreContext);

export default GlobalStore;

We will now create a combineReducer function, just like Redux to combine multiple reducers if needed. The combineReducer.js file will look like this:

const combineReducer = reducers => {
  const finalReducers = {};

  Object.keys(reducers).forEach(key => {
    if (typeof reducers[key] === "function") finalReducers[key] = reducers[key];
    else throw new Error(`${key} reducer must be a function`);
  });
  const finalReducerKeys = Object.keys(finalReducers);

  return (state, action) => {
    let hasChanged = false;
    const nextState = {};
    finalReducerKeys.forEach(key => {
      const reducer = finalReducers[key];
      const previousStateForKey = state[key];

      const nextStateForKey = reducer(previousStateForKey, action);
      nextState[key] = nextStateForKey;
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
    });

    hasChanged =
      hasChanged || finalReducerKeys.length !== Object.keys(state).length;
    return hasChanged ? nextState : state;
  };
};

export default combineReducer;

We will also create a createStore.js file just to create an object out of reducer and initialState. It's so trivial.

const createStore = (reducer, initialState) => ({ reducer, initialState });

export default createStore;

This whole code can be found here.

Well, that was easy. We have our lightweight react-store ready to be used. In next article, we will create a simple app and try to use this global store firsthand. That will be exciting. Let me quote Charles Bukowski at the end:

If you’re going to try, go all the way. There is no other feeling like that. You will be alone with the gods, and the nights will flame with fire. You will ride life straight to perfect laughter. It’s the only good fight there is.