Consuming state with React Context API
Foreword
Not so long time ago the React Team has released a shiny new version of its library (16.3.0) which contains a lot of new features. The one that I am most excited about is the new Context API.
You probably know that in previous versions there was a kind of urge to not to use Context API because of its error-prone nature. Things have changed and we can get our hands dirty now. The new Context API lets us easily avoid the "prop drilling" (or the "prop hell" as I call it) and have an access to the parent component state no matter how deep or complicated hierarchy we have.
Why?
Of course, the same thing we can do by using some external libraries like Redux, but there are situations when you might not need Redux. Instead, you need to have a clear state structure and no prop drilling.
Let's illustrate this by building simple 3-slider HSL color picker. The state object of this app would be very simple:
{
hue: 180,
saturation: 50,
lightness: 50
}
A Single Source Of Truth for all subcomponents. We're gonna update it using methods stored in the methods
object. All that stuff will be contained in a component called Provider
.
Context and initial state
To use the Context API, we must invoke React.createContext
method. Then we have to create the InitialState
object and add the Provider
class to the code. InitialState
will be a set of default values for our app and the Provider
will be declared after the createContext()
. It will use Context.Provider
to wrap the children elements passed to it.
const Context = React.createContext()
const InitialState = {
hue: 180,
saturation: 50,
lightness: 50
}
class Provider extends React.Component {
constructor(props) {
super(props)
this.state = {}
}
render() {
return (
<Context.Provider
value={{
state: this.state
}}
>
{this.props.children}
</Context.Provider>
)
}
componentDidMount() {
this.setState({ ...InitialState })
}
}
Take a note how it goes. <Context.Provider>
has an attribute called value
. You may think of it as the store in Redux. It's correct (I guess), as the value
stores the state (set to Provider state, set to InitialState after Provider
mounts) and the methods (which we will add later).
Pick a color (and Provide it)
To pick a color, we must have three sliders. One for a hue
, one for saturation
, and one for the lightness
(you can choose HSB not HSL color model as the root space though, it would be even better). We also need one component to rule them all. A Slider
.
We'll wrap all Slider
s in a Picker
component. The Picker
will have a state, that help us to render all Slider
s dynamically. The Picker
state will have a data
property, which will be an array of objects containing type
, min
, max
and step
values. Because our sliders will be fully controlled components, they'll also have a value
and onChange
props that deal straight with the Context API.
To make the state available for all sliders in the Picker
we must wrap it in a <Context.Consumer>
and then use the render props pattern. Context.Consumer
requires a function as a child, not an element.
To demonstrate no-prop-drill ability, I added a Middle
component that prevents displaying the Picker
straight in the Provider
.
See the Pen Gradient builder post 1 by Kuba (@jakub_antolak) on CodePen.
See? No prop-drilling at all!
We would want also to instantiate our Context.Consumer
in the same handy way like in Provider
case. Let's wrap it in the Consumer
component then.
const Consumer = ({ render }) => (
<Context.Consumer>
{context => render(context)}
</Context.Consumer>
)
render
parameter takes a function as a parameter, which has context as its parameter.
This piece of code is a little bit tricky. We could (or even should) pass a prop object into render
, like this: {context => render({ context })}
. This would follow a common prop pattern. However, because we want to make render
to be not only a stateless component, but also a stateful class, this would not work. In React classes cannot be called as functions, and so render={Picker}
or render={<Picker />}
won't work.
To work it around, we must use render props
pattern again. Let's insert a function to render
when the Consumer
is invoked.
const Consumer = ({ render }) => (
<Context.Consumer>
{context => render(context)}
</Context.Consumer>
)
const Middle = () => (
<div>
<Consumer render={context => <Picker context={context} />} />
</div>
)
Instead of calling a class like it was a function (which is forbidden), we return it (and invoke at the same time) straight after the context
is provided. Observe when this ("context") word appears.
To highlight the importance of using a function (and only a function), let's add some propTypes
to the Consumer
.
Consumer.propTypes = {
render: PropTypes.func.isRequired
}
If you want to include some other props to the "consumed" Picker
, just pass them to it.
let _props = { otherType: 'some data'}
const Middle = () => (
<div>
<Consumer
render={context => (
<Picker context={context} {..._props}/>
)}
/>
</div>
)
We can go another way and instead of passing a render-prop function as a render
prop value, put it as a Consumer
s child. All we need to do is to make props.children
a function.
const _Consumer = props => (
<Context.Consumer>
{context => props.children(context)}
</Context.Consumer>
)
// then in Main.render()
<_Consumer>
{context => <Picker context={context} {..._props} />}
</_Consumer>
However, what I think is the best approach (and what took me some time to reach it) is to go with Higher Order Component pattern, which functionally provides the same behavior, but gives us the most easy to use solution.
Let's just put our Picker
in a function that returns him inside the Context.Consumer
(which is returned inside a class component) and call this function withConsumer
.
function withConsumer(WrappedComponent, data = {}) {
return class extends React.Component {
constructor(props) {
super(props)
}
render() {
return (
<Context.Consumer>
{context => (
<WrappedComponent
context={context}
{...data}
/>
)}
</Context.Consumer>
)
}
}
}
const SuperPicker = withConsumer(Picker, _props)
Boom, it works!
See the Pen Gradient builder post 2 by Kuba (@jakub_antolak) on CodePen.
All three solutions give us the ability to pass the data to wrapped Picker
component easily. However - only with the HOC approach we can avoid multiple wrappings which results in a cleaner code. With HOC we can also add more extra functionalities to the returned class.
Afterword
I hope this chunk of text helped you understand how to handle the component state management with the new React Context API. If you noticed some serious bugs in what I wrote here, feel free to reach out in the comments. After all - this post was created to help myself understand (and play around with) the Context API, as well as the render props and HOC patterns 😅. Thanks!
Appendix
If you don't want to add additional functionalities to the HOC Component, you can simplify it by using modified props.children solution described above. Just make it stateless.
See the Pen Context API 3 by Kuba (@jakub_antolak) on CodePen.