loading...
Cover image for React's new Context API and Actions

React's new Context API and Actions

washingtonsteven profile image Steven Washington Updated on ・5 min read

Photo: Daniel Watson

Edit: 4/2/2018 - It was pointed out to me that the example in this post had a performance issue, where render was called on Consumers unnecessarily. I've updated the article, examples, and the CodeSandbox to rectify this.

The new React Context API (coming soon now here! in React 16.3) is a massive update of the old concept of context in React, which allowed components to share data outside of the parent > child relationship. There are many examples and tutorials out there that show how to read from the state provided by context, but you can also pass functions that modify that state so consumers can respond to user interactions with state updates!

Why Context?

The context API is a solution to help with a number of problems that come with a complex state that is meant to be shared with many components in an app:

  1. It provides a single source of truth for data that can be directly accessed by components that are interested, which means:
  2. It avoids the "prop-drilling" problem, where components receive data only to pass it on to their children, making it hard to reason about where changes to state are (or aren't) happening.

B-but Redux!

Redux is a fantastic tool that solves these problems as well. However Redux also brings a lot of other features to the table (mostly around enforcement of the purity of state and reducers) along with required boilerplate that may be cumbersome depending on what is needed. For perspective, Redux uses the (old) context API.

Check out this article by Dan the Man himself: You Might Not Need Redux

What's Context do?

There are plenty of articles on this (I particularly like this one), so I don't want to go into too many details about how this works. You've seen the examples so far, and they're mostly missing something: how to update the state in the provider. That state is sitting there, and everyone can read it, but how do we writeto it?

Simple Context Example

In many of these examples we make a custom provider to wrap around React's, which has its own state that is passed in as the value. Like so:

context.js

import React from "react";

const Context = React.createContext();

export class DuckifyProvider extends React.Component {
  state = { isADuck: false };
  render() {
    const { children } = this.props;
    return ( 
      <Context.Provider value={this.state}>
        {children}
      </Context.Provider>
    );
  }
}

export const DuckifyConsumer = Context.Consumer;

Seems simple, enough. Now we can use the DuckifyConsumer to read that state:

DuckDeterminer.js

import React from "react";
import { DuckifyConsumer } from "./context";

class DuckDeterminer extends React.Component {
  render() {
    return (
      <DuckifyConsumer>
        {({ isADuck }) => (
          <div>
            <div>{isADuck ? "quack" : "...silence..."}</div>
          </div>
        )}
      </DuckifyConsumer>
    );
  }
}

export default DuckDeterminer;

Passing Functions

Now, what if we wanted to emulate a witch turning something into a duck (stay with me here)? We need to set isADuck to true, but how?

We pass a function.

In Javascript, functions are known as "first-class", meaning we can treat them as objects, and pass them around, even in state and in the Provider's value prop. It wouldn't surprise me if the reason why the maintainers chose value and not state for that prop is to allow this separation of concepts. value can be anything, though likely based on state.

In this case, we can add an dispatch function to the DuckifyProvider state. dispatch will take in an action (defined as a simple object), and call a reducer function (see below) to update the Provider's state (I saw this method of implementing a redux-like reducer without redux somewhere, but I'm not sure where. If you know where, let me know so I can properly credit the source!).

We pass the state into the value for the Provider, so the consumer will have access to that dispatch function as well.

Here's how that can look:

context.js

import React from "react";

const Context = React.createContext();

const reducer = (state, action) => {
  if (action.type === "TOGGLE") {
    return { ...state, isADuck: !state.isADuck };
  }
};

export class DuckifyProvider extends React.Component {
  state = {
    isADuck: false,
    dispatch: action => {
      this.setState(state => reducer(state, action));
    }
  };
  render() {
    const { state, props: { children } } = this;
    return <Context.Provider value={state}>{children}</Context.Provider>;
  }
}

export const DuckifyConsumer = Context.Consumer;

Note that we have dispatch in our state, which we pass into value. This is due to a caveat in how the need to re-render a consumer is determined (Thanks, Dan for pointing that out!). As long as the reference to this.state stays pointed to the same object, any updates the make the Provider re-render, but don't actually change the Provider's state, won't trigger re-renders in the consumers.

Now, in DuckDeterminer, we can create an action ({type:"TOGGLE"}) that is dispatched in the button's onClick.

(We can also enforce certain action types with an enum object that we export for the DuckifyContext file. You'll see this when you check out the CodeSandbox for this)

DuckDeterminer.js

import React from "react";
import { DuckifyConsumer } from "./DuckContext";

class DuckDeterminer extends React.Component {
  render() {
    return (
      <DuckifyConsumer>
        {({ isADuck, dispatch }) => {
          return (
            <div>
              <div>{isADuck ? "🦆 quack" : "...silence..."}</div>
              <button onClick={e => dispatch({ type: "TOGGLE" })}>
                Change!
              </button>
            </div>
          );
        }}
      </DuckifyConsumer>
    );
  }
}

export default DuckDeterminer;

The secret sauce here is the dispatch function. Since we can pass it around like any other object, we can pass it into our render prop function, and call it there! At that point the state of our Context store is updated, and the view inside the Consumer updates, toggling on and off whether the duck truly exists.

Extra credit

You can (read: I like to) also add a helpers field alongside state and dispatch, as a set of functions that "help" you sift through the data. If state is a massive array, perhaps you can write a getLargest or getSmallest or getById function to help you traverse the list without having to split the implementation details of accessing various items in a list in your consumer components.

Conclusion

Used responsibly, the new Context API can be very powerful, and will only grow as more and more awesome patterns are discovered. But every new pattern (including this one, even) should be used with care and knowledge of the tradeoffs/benefits, else you're dipping your toes in deaded antipattern territory.

React's new context API, is incredibly flexible in what you can pass through it. Typically you will want to pass your state into the value prop to be available to consumers, but passing along functions to modify state is possible as well, and can make interacting with the new API a breeze.

Try it out

The DuckDeterminer component is available to play with on CodeSandbox, right now!

Posted on Mar 31 '18 by:

washingtonsteven profile

Steven Washington

@washingtonsteven

Full stack web dev with a lot of experience in JS and PHP, constantly sniffing around to get more broad with skills and languages.

Discussion

markdown guide
 

Thank you! I was looking for: "react handle function passed through context" and I found your post. Despite the fact that I've taken my time to understand Redux, I still find it to much bloat. Probably because "I probably don't need it". Nonetheless, I need some of its features. Your pattern hits a sweet spot. Using this as my pattern for now

import React, { Component, createContext } from 'react';
import PropTypes from 'prop-types';

const reducer = (state, action) => {
    const { type, ...rest } = action;
    if (type === 'SET_STATE') {
        return {
            ...state,
            ...rest,
        };
    }
};

// export context so that SomeComponentThatNeedContext
// can be augmented withContext using: react-context-consumer-hoc
// const EnhancedComponent = withContext(Context)(SomeComponentThatNeedContext)
export const Context = createContext();

export class Provider extends Component {
    static propTypes = {
        children: PropTypes.any,
    };
    state = {
        dispatch: action => {
            this.setState(state => reducer(state, action));
        },
    };
    render() {
        const {
            state,
            props: { children },
        } = this;
        return <Context.Provider value={state}>{children}</Context.Provider>;
    }
}

// Alternative to wrap the parent component with the Provider
export const withProvider = Component => {
    return function ComponentWithProvider(props) {
        // ... and renders the wrapped component with the Provider
        return (
            <Provider>
                <Component {...props} />
            </Provider>
        );
    };
};
 

Note your example will re-render consumers more often than necessary.

reactjs.org/docs/context.html#caveats

 

Thanks for pointing that out! I made some updates to that example that should address this.

always important to RTFM; I messed up in this case. 😳

Always learning!

 

I'm not too familiar with Redux so I have a few questions about this method.

1) How would I fire off multiple actions at once> For instance if my component needs to change 2-3 state values? Can I just pass multiple actions? What does that look like?

2) What if my state/action doesn't take a true/false vale but instead changes a string? For example:

state = {
msg: 'This message can change',
dispatch: action => {
this.setState(state => reducer(state, action));
}
};

How would I pass in a new msg to the action at this point to change the msg?

Thanks so much for the guidance!

 

1) I would say that you could fire multiple calls to dispatch for each of your actions, or (if possible) create a new action type (a so-called "super-action") that's actually a collection of other actions.

2) Keep in mind that you action can be any sort of object. So in addition to type (which exists so you know what action you are working with), you can add in other data. An action can look like:

{
  type: "UPDATE_STRING",
  newString: "This is the new string!"
}

And then the reducer would update the state by using action.newString

 

Hello, nice post.
In my opinion, Context API is handy for simple apps. But for more complex apps, where an event-driven model would more suitable, I can't see how the Context API would solve easier the problems that Redux middlewares already do.

 

Hi,
I am using the way you are explaining in this post but i am facing an issue.

my provider state looks like :
export class CalendarProvider extends React.Component {
state = {
content: '',
dispatch: action => {
this.setState (state => CalendarReducer (state, action));
},
};

with the CalendarReducer doing :
const CalendarReducer = (state, action) => {
switch (action.type) {
case 'CONTENT': {
return Object.assign (state, {content: action.payload});
}
}
};

I would like to clone the Provider state object in my component state :
class Calendar extends Component{
//my component state = clone of the provider state
this.state = Object.assign ({}, this.props.store);
}

Now I have 2 differents object :

  • this.state (calendar State)
  • this.props.store (Calendar Provider State)

I can modify both of them doing :

  • this.state = object.assign(this.state,{content:'test'})
  • this.props.store = object.assign(this.props.store,{content:'retest'}) result : this.state ={ content:'test' } this.props.store ={ content:'retest' }

both value are changed independently.

BUT, when I am trying to do the same thing with the dispatch function, it only change the state of the provider :

this.state.dispatch({type:'CONTENT',payload:'dev'})
The result is :
this.state ={
content:'test'
}
this.props.store ={
content:'dev'
}

instead of :
this.state ={
content:'dev'
}
this.props.store ={
content:'retest'
}

The dispatch function only update the provider component.
the this.state.dispatch point to this.props.store.dispatch.

How can i do to have 2 different function :
this.state.dispatch that update this.state (the consumer)
and
this.props.store.dispatch that update this.props.store (the provider)

I hope it is clear enough
And hopefully you know where is my mistake

Thank you

 

I just wanted to share functional component approach of above implementation.

Here you go!

codesandbox.io/s/duckify-xw555?fon...

It's my favorite one 🙌
v1

v2

 

So React now is more than just a view library?

 

React has always had state management built-in. This is just another way of updating the front-end state; no assumptions are made about any back-end architecture.

In theory, instead of your action directly modifying the context state, the action could kick off a request to your backend to update its data, which then responds with updated (and server-validated) data that you can use to update your front-end store.

In that sense, React is still just a view layer, showing the data that you pass into its state and responding to user events. It's up to you to decide what sort of data updates that triggers, and React will come along for the ride. 😀

 
 

Please tell me in the dispatch what basically doing an "action"?

 

Hey, thx for your post, I was wondering how to do something similar to this, right now I'm just thinking in how to manage multiple reducers and import them at once