DEV Community

loading...

Use React Hooks & Context API to build a Redux style state container

Joel Raju
I write code and sometimes people pay me for it.
Originally published at joelraju.com Updated on ・4 min read

Originally published on my blog.

Update

This approach is best suited for low frequency state updates. Please check the performance section for more details.

State management is hard

State management is hard get right in complex React apps for most of us. State can include UI state like routes, form states, pagination, selected tabs, etc as well as the response from http calls, loading states, cached data etc.

Even at Facebook, they had difficulty in showing the correct notification count for chat messages.

The necessity to tame this increasing complexity gave rise to some interesting libraries and paradigms.

Some of the popular state-management libraries out there:

Redux might be the single most popular library used in tandem with React. It popularized the notion of uni-directional flow of data and made state updates predictable and manageable.

We'll try to build a utility with the same principles in mind, a single source of truth with uni-directional flow of data where state updates are performed by dispatching an action (pure functions).

Context API

Context provides a way to pass data through the component tree without having to pass props down
manually at every level.

Context is a powerful tool to have. In fact, Redux binding for React
itself uses the Context API. Along with the useReducer & useContext hooks we have all the pieces to build our state management utility.

Demo time

We'll be building a basic counter with 2 buttons to increment and decrement the count. Our global store will have a single piece of state called count. The demo will be using Typescript.

Building the global store and the reducer

First lets create the context object. It will have two properties the state object itself and the dispatch function.

// ...

const GlobalStateContext = createContext<{
  state: State;
  dispatch: (action: Action) => void;
}>({ state: INITIAL_STATE, dispatch: () => {} });

// ...

When React renders a component that subscribes to this Context object it will read the current context value from the closest matching Provider above it in the tree.

The reducer function is fairly the same as a Redux reducer, which performs state updates on incoming Action and then returning the new state.

Putting it all together.

import { createContext, Reducer } from 'react';
import { ActionTypes } from './globalActions';

interface State {
  count: number;
}

export const INITIAL_STATE: State = {
  count: 0
};

export interface Action {
  type: ActionTypes;
  payload?: any;
}

export const GlobalStateContext = createContext<{
  state: State;
  dispatch: (action: Action) => void;
}>({ state: INITIAL_STATE, dispatch: () => {} });

export const globalReducer: Reducer<State, Action> = (state, action) => {
  const { type } = action;
  switch (type) {
    case ActionTypes.INCREMENT:
      return { ...state, count: state.count + 1 };
    case ActionTypes.DECREMENT:
      return { ...state, count: state.count - 1 };
    default:
      return state;
  }
};

We have 2 actions INCREMENT & DECREMENT and corresponding action creators which dispatches those actions.

export enum ActionTypes {
  INCREMENT = 'INCREMENT',
  DECREMENT = 'DECREMENT'
}

export const incrementAction = () => ({
  type: ActionTypes.INCREMENT
});

export const decrementAction = () => ({
  type: ActionTypes.DECREMENT
});

Connecting the store to the components

Every Context object comes with a Provider React component that allows consuming components to subscribe to context changes. It receives a prop value consuming components that are descendants of this Provider.

useReducer is a hook that accepts the reducer and the initial state and returns the current state paired with a dispatch method. (If you’re familiar with Redux, you already know how this works.)

We need to wrap the root component of our app in the Provider, and pass the returned state and dispatch as the value prop.

// ...

const [globalState, dispatchToGlobal] = React.useReducer(
  globalReducer,
  INITIAL_STATE
);

return (
  <GlobalStateContext.Provider
    value={{ state: globalState, dispatch: dispatchToGlobal }}
  >
    <div className='App'>
      <Layout />
    </div>
  </GlobalStateContext.Provider>
);

// ...

At this point, our entire app has access to the global state and can dispatch actions to the store. Now lets connect the UI components to the store.

The useContext hook accepts a Context object and returns the current context value for that context, which in our case is the state & dispatch method.

import * as React from 'react';
import { GlobalStateContext } from './context/globalStore';
import { incrementAction, decrementAction } from './context/globalActions';

const Layout: React.FC = () => {
  const { state, dispatch } = React.useContext(GlobalStateContext);

  return (
    <div>
      <div>
        <h2>Count : {state.count}</h2>
      </div>
      <div>
        <button onClick={() => dispatch(incrementAction())}>Increment</button>
        <button onClick={() => dispatch(decrementAction())}>Decrement</button>
      </div>
    </div>
  );
};

export default Layout;

What about performance ?

As pointed out by @pinutz23 , this approach is suited for low frequency state updates. React Redux uses context internally but only to pass the Redux store instance down to child components - it doesn't pass the store state using context. It uses store.subscribe() to be notified of state updates.

Passing down the store state will cause all the descendant nodes to re-render.

See more about this here

Souce code

Checkout the full source at CodeSandbox

Conclusion

The state management utility we created here shows what's possible with React Hooks & Context API. This approach as it is, without any performance optimizations, is best suited for low frequency state updates like theme, localization, auth, etc. For high frequency updates I still use Redux and you should try it too.

Discussion (2)

Collapse
jannikwempe profile image
Jannik Wempe • Edited

Thanks for that article. It explains the usage of the context API quite good, BUT...

In my opinion it is a bad practise. And I'll tell why: All components consuming a context provides will re-render if the value of the context changes. Quote from react documentation:

All consumers that are descendants of a Provider will re-render whenever the Provider’s value prop changes.

This is why one global state is not recommended and context API is better suited for state which is not changing frequently (theme, localization, auth...). If you use context API you should split the context. See this comment of the redux co-author.

Maybe now you're asking yourself: What is the difference between using context API and redux (also using context API). As the maintainer @markerikson has recently written on his blog it is a common misconception that redux is just some kind of a wrapper around the context API.

tl;dr
Do not use a single global context for state management.

Collapse
joelraju profile image
Joel Raju Author

Thanks for pointing this out. I'll update the post with the performance implications.