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:
- It provides a single source of truth for data that can be directly accessed by components that are interested, which means:
- 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!
Top comments (13)
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
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:And then the reducer would update the
state
by usingaction.newString
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 :
I can modify both of them doing :
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 π
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.
Worth noting that I've got an open proof-of-concept PR that updates React-Redux to use the new context API.
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. π
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
Please tell me in the dispatch what basically doing an "action"?