React State Context

Lightweight state management using React Context.

✔ Built on React primitives
✔ Provides a familiar API
✔ Designed with a minimal learning curve in mind
✔ Reasonable file size (~2kb gzipped)

Motivation

When you are getting started with React, storing all of your application state within individual Components'
state tends to work well.
Component state is a good solution for storing data.

A limitation of component state is that it can be tedious to share it between components
that are not near one another within your application's component tree. This problem may become more pronounced
as your application grows, and as some of your data needs to be made available to a larger number of separated
components.

React provides an API to solve this problem: Context. Context is a
mechanism to more easily share data between components, even when they are not close.

As delightful as the Context API is, it is a low-level tool, so using it directly can be a little verbose sometimes.
It also doesn't provide opinions on how it should be used, so it can take some time to figure out an organized system for
working with it. Lastly, it has some caveats that can trip you up. For these
reasons I created React State Context.

React State Context is a thin wrapper around Context that provides a small amount of structure. This structure
helps reduce the boilerplate that you must write, and it also helps you to stay organized. Plus, when you use State
Context, you can be confident that you are avoiding the caveats that accompany using Context directly.

Installation

Install using npm:

npm install react-state-context

or yarn:

yarn add react-state-context

Concepts

React State Context has three concepts.

StateContext

A StateContext is a wrapper around a normal React Context object. Like a regular Context object, it has two properties:
Provider and Consumer.

You use StateContexts in the same way as regular Context. If you have used the new Context API, then it should feel
familiar to use StateContexts. If you haven't, don't worry. If I was able to learn it, then you can, too!

:information_desk_person: The React documentation on Context
is a great resource. It can be helpful to familiarize yourself with the content on that page before using State Context.

What is different about a StateContext is that the value that the Consumer provides you with has the
following structure:

{
  state,
  ...actions
}

State and actions are the other two concepts of React State Context. Let's take a look!

State

Every StateContext object has an internal state object. Behind the scenes, it is a regular Component's state object. When you render
a StateContext.Consumer, the value passed to the render prop will include a state attribute.

<MyStateContext.Consumer>
  {value => {
    console.log('The current state is:', value.state);
  }}
</MyStateContext.Consumer>

Like a React Component's state, the StateContext state must be an object or null.

Actions

Actions are functions that you define, and they are how you modify the state. If you have used Redux, then you can
think of them as serving a similar role to action creators.

To update state, return a new value from your action. The returned value will be shallowly merged
with the existing state.

Let's take a look at an example action:

export function openModal() {
  return {
    isOpen: true,
  };
}

When you call an action from within your application, you can pass arguments to it. Let's use this to
create an action to toggle a modal state based on what is passed into the action:

export function toggleModal(isOpen) {
  return { isOpen };
}

Sometimes, you may need the previous state within an action. In these situations, you can return a
function from your action. This function will be called with one argument, setState. Use setState to update
the state as you would using a React Component's setState:

export function createTodo(newTodo) {
  return function(setState) {
    setState(prevState => {
      // Shallow clone our todos, so that we do not modify the state
      const clonedTodos = [...prevState.todos];

      return {
        todos: clonedTodos.push(newTodo),
      };
    });
  };
}

Note that setState differs from the Component setState in that there is no second argument.

:information_desk_person: Heads up! The actions API was inspired by redux-thunk.
If you have used that API, you may notice the similarity. In redux-thunk, the thunks are passed the arguments (dispatch, getState).
In this library, you are passed (setState).

Along with state, the actions that you define will be included in the value that you receive from the Consumer:

<MyStateContext.Consumer>
  {value => {
    console.log('I can add a todo using:', value.createTodo);
    console.log('I can delete todos using:', value.deleteTodo);
  }}
</MyStateContext.Consumer>

Once you feel comfortable with these concepts, you are ready to start using React State Context.

API

This library has one, default export: createStateContext.

createStateContext( actions, [initialState] )

Creates and returns a StateContext.

  • actions [Object]: The actions that modify the state.
  • [initialState] [Object|null]: Optional initial state for the StateContext.
import createStateContext from 'react-state-context';
import * as todoActions from './actions';

const TodoContext = createStateContext(todoActions, {
  todos: [],
});

export default TodoContext;

Use a StateContext as you would any other Context.

import TodoContext from './contexts/todo';

export function App() {
  // To begin, you must render the Provider somewhere high up in the Component tree.
  return (
    <TodoContext.Provider>
      <SomeComponent />
    </TodoContext.Provider>
  );
}
import TodoContext from './contexts/todo';

export function DeeplyNestedChild() {
  // From anywhere within the Provider, you can access the value of the StateContext.
  return (
    <TodoContext.Consumer>
      {value => {
        console.log('The current state is', value.state);
        console.log('All of the todos are here', value.state.todos);

        console.log('I can add a todo using:', value.createTodo);
        console.log('I can delete todos using:', value.deleteTodo);
      }}
    </TodoContext.Consumer>
  );
}

GitHub