Erik Aybar

Making React Context even easier: createReducerContext

June 11, 2019

This is really just a quick note on an idea I’m playing with I wanted to share. I’ve found that the friction of piecing together contexts steers me towards lumping sometimes unrelated state into a single, bloated context. This is a solution I’m exploring to reduce that friction and provide all the pieces required to both provide and consume reducer state via React context.

This createReducerContext(reducer, initialState) wraps up the approach described in https://kentcdodds.com/blog/how-to-use-react-context-effectively in an easy to consume API. Given a reducer, initial state, and accompanying callable type arguments this produces:

  • Provider: React.ComponentType<{children: React.Node}>
  • useState(): State
  • useDispatch(): Dispatch<Action>
  • useContext(): [State, Dispatch<Action>]

Which provides optimized state and dispatch Contexts (further reading: https://kentcdodds.com/blog/how-to-optimize-your-context-value) and hooks to consume only state, dispatch, or both easily.

createReducerContext usage example:

createReducerContext usage example

The main obstacle I ran into was preserving Flow typing so that consumers had all the type information as they did before I extracted this into a context factory. I found that I had to explicitly type the return value of createReducerContext to achieve this.

Here’s the code in its entirety:

// createReducerContext.js

// @flow
import * as React from 'react';

type Reducer<State, Action> = (state: State, action: Action) => State;
type Dispatch<Action> = (action: Action) => void;

/**
 * Easily create context Provider and consumer hooks for reducer state.
 * Inspired by https://kentcdodds.com/blog/how-to-use-react-context-effectively
 */
export function createReducerContext<State, Action>(
  reducer: Reducer<State, Action>,
  initialState: State
): {
  Provider: React.ComponentType<{children: React.Node}>,
  useState(): State,
  useDispatch(): Dispatch<Action>,
  useContext(): [State, Dispatch<Action>],
} {
  const StateContext = React.createContext<State>(initialState);
  const DispatchContext = React.createContext<Dispatch<Action>>(() => {});

  function Provider({children}) {
    const [state, dispatch] = React.useReducer<State, Action>(
      reducer,
      initialState
    );

    return (
      <DispatchContext.Provider value={dispatch}>
        <StateContext.Provider value={state}>{children}</StateContext.Provider>
      </DispatchContext.Provider>
    );
  }

  function useState() {
    return React.useContext(StateContext);
  }

  function useDispatch() {
    return React.useContext(DispatchContext);
  }

  function useContext() {
    return [useState(), useDispatch()];
  }

  return {
    Provider,
    useState,
    useDispatch,
    useContext,
  };
}

So you can imagine how this would reduce friction as you have multiple reducer contexts such as:

// SearchContext.js
const {
  Provider: SearchProvider,
  useState: useSearchState,
  useDispatch: useSearchDispatch,
  useContext: useSearchContext,
} = createReducerContext<State, Action>(reducer, initialState);

export {SearchProvider, useSearchState, useSearchDispatch, useSearchContext};
// UserContext.js
const {
  Provider: UserProvider,
  useState: useUserState,
  useDispatch: useUserDispatch,
  useContext: useUserContext,
} = createReducerContext<State, Action>(reducer, initialState);

export {UserProvider, useUserState, useUserDispatch, useUserContext};
// WhateverContext.js
const {
  Provider: WhateverProvider,
  useState: useWhateverState,
  useDispatch: useWhateverDispatch,
  useContext: useWhateverContext,
} = createReducerContext<State, Action>(reducer, initialState);

export {WhateverProvider, useWhateverState, useWhateverDispatch, useWhateverContext};

I’d really like to explore how to best publish this as an npm module. create-react-library looks promising.



Erik Aybar

👋🏽 Hi! I'm Erik Aybar. I'm a software person working remotely from St. George, Utah. This is my blog.