In my previous blog post, I explained why we need a state management library like Redux and little bit of how Redux works. In this blog post I will share how we can build a redux-like library with the help of the concepts provided by React.
React comes with hooks and also it has a powerful context API. We are going to take advantage of these two features and build a state management system. This post expects you to have little knowledge of React hooks and context API.
What is React context ?
Context provides a way to pass data through the component tree without having to pass props down manually at every level.
This is exactly what we need. Context solves the problem of sharing data between different components. So we can create a global store which we will keep in the context along with a dispatch function.
Before we jump into creating a store, lets code our app first and then go backwards on creating the global store.
Application - Counter
We are going to develop a simple counter and use three components.
- Main app component - App.js
- Display counter - Counter.js
- Increment counter - IncrementCounter.js
We will be incrementing the counter from a different component and see it getting updated in the Counter.js component. So lets go ahead and create these three components.
import React from "react";
/**
* We will create the below functions later.
*/
import StoreProvider, { useGlobalStore } from "./store";
// App component
const App = () => {
return (
<StoreProvider>
<IncrementCounter />
<Counter />
</StoreProvider>
);
};
// Counter component to display the count
const Counter = () => {
const [store, dispatch] = useGlobalStore();
return <div> Counter: {store.counter} </div>;
}
// IncrementCounter component
const IncrementCounter = () => {
const [store, dispatch] = useGlobalStore();
const add = () => {
const counter = store.counter;
dispatch({
type: 'INCREMENT_COUNTER',
payload: counter + 1
});
}
return <button onClick={add}>Increment counter</button>;
}
In the above snippet, whenever we click the button, the dispatcher triggers an action which is received by the reducer. The reducer is then responsible to update the global store. So lets go ahead and create a reducer.
const initialState = { counter: 0 };
const reducer = (state,action) => {
switch (action.type) {
case "INCREMENT_COUNTER": {
return { ...state, counter: action.payload };
}
}
return state;
}
Now that our app is ready, lets complete the missing parts, **StoreProvider **and useGlobalStore.
Creating a Provider with Context API
Context provides us with a provider and consumer. We can use the provider to pass data and then using the consumer, we can consume this data from any component. So first, lets create a context.
const initialState = {counter: 0};
const dispatch = () => {};
const GlobalStateContext = createContext([initialState,dispatch]);
Now that we have a context, lets create a provider. The provider will be at the top level and we will wrap our application inside this provider. So our application will be available as props.children.
import { useReducer, createContext } from "react";
import reducer from "./reducer";
const initialState = {counter: 0};
const dispatch = () => {};
const GlobalStateContext = createContext([initialState,dispatch]);
function StoreProvider(props) {
// make globalState and dispatch available to all components
const value = useReducer(reducer, initialState);
return (
<GlobalStateContext.Provider value={value}>
{props.children}
</GlobalStateContext.Provider>
);
}
Notice the usage of useReducer from React. We passed the reducer and the initialState to useReducer, which gives us the value (array) containing the store and a dispatch function. We then pass this value to the provider so that we can access it from any child component.
Creating a Consumer
Lets create a hook that can access the value of the context. React gives us the method useContext. We just need to pass the GlobalStateContext to this.
import { useContext } from "react";
const useGlobalStore = () => {
return useContext(GlobalStateContext);
};
Now you can access the global store from any component just like this:
const [store, dispatch] = useGlobalStore();
Combining everything together, our complete app will look like this.
import React, { useContext, useReducer, createContext } from "react";
const initialState = { counter: 0 };
const dispatch = () => {};
const GlobalStateContext = createContext([initialState,dispatch]);
const reducer = (state,action) => {
switch (action.type) {
case "INCREMENT_COUNTER": {
return { ...state, counter: action.payload };
}
}
return state;
}
function StoreProvider(props) {
// make globalState and dispatch available to all components
const value = useReducer(reducer, initialState);
return (
<GlobalStateContext.Provider value={value}>
{props.children}
</GlobalStateContext.Provider>
);
}
const useGlobalStore = () => {
// We just need to pass the GlobalContext.
return useContext(GlobalStateContext);
};
// Counter component to display the count
const Counter = () => {
const [store, dispatch] = useGlobalStore();
return <div> Counter: {store.counter} </div>;
}
// IncrementCounter component
const IncrementCounter = () => {
const [store, dispatch] = useGlobalStore();
const add = () => {
const counter = store.counter;
dispatch({
type: 'INCREMENT_COUNTER',
payload: counter + 1
});
}
return <button onClick={add}>Increment counter</button>;
}
const App = () => {
return (
<StoreProvider>
<IncrementCounter />
<Counter />
</StoreProvider>
);
};
You can try this out here - Demo
Summary
- We saw how we can use Reacts useContext, useReducer and createContext to create a global store.
- We saw how to create a GlobalStateContext using createContext by passing an array containing initial state and a function.
- We created a StoreProvider using the GlobalStateContext and passed the value from React's useReducer to it.
- We then created a consumer hook useGlobalStore which internally uses React's useContext.
I hope this post has given you some ideas on how to build your next store. If you have any suggestions, tweet me at @__abhisaha