Back in 2015, I was developing applications using Backbone and Marionette, when a young developer approached me to tell me something exciting. "Check out React", he said. I have heard this before with AngularJs, with KnockoutJs, and during that time, every second developer was developing a framework anyway.

As a team lead, I was still not fully convinced about the need to switch to React, but I joined the presentation of my colleague. Back in the days, he said, React was the V in MVC. In hindsight, this statement turned out to be false. Frankly, in the Model-View-Presenter world of Backbone this statement didn't even make sense to me. Why? Because in the Backbone world, the View layer just contains dumb templates that the Presenter component prepares.

The intention behind this statement was to signal that React didn't really manage the application state in a sustainable way. It was possible to use React and only React, but then you had to pass down the state as props. Kent C. Dodds calls this technique prop drilling. Prop drilling makes it harder and harder to maintain your application as the application size grows. By the way, if you clicked on the featured image of this article and you saw a crossed out drilling machine, now you may understand the odd choice of graphics.

The community already found an answer by the time I wrote my first React line: keep business data in a store, residing outside the components. Any component should be able to access the store, as long as they follow some rules that ensure the maintainability of the solution.

The Flux architecture, and some very popular implementations, such as Redux and MobX took care of handling data in a maintainable way. I have used both Redux and MobX, and they are both great combinations for managing the application state. Redux was built based on the principle of making the application state immutable, while MobX allowed the mutation of application state.

Unfortunately, both Redux and MobX introduced the complexity of developing an application, and they didn't seem right for small applications.

If you are new to react, you may not yet know what prop drilling is, therefore, let me illustrate the problem with an example.

Managing Application State with Prop Drilling

Imagine you have a React application symbolizing a poker game. In this game, you have a PokerHand stateless functional component, displaying the two cards you are holding.

import React from 'react'; const PokerHand = (props) => ( <div className="hand"> Hand: {props.cards.join( '-' )} </div> ); 1 2 3 4 5 6 7 8 9 import React from 'react' ; const PokerHand = ( props ) = > ( < div className = "hand" > Hand : { props . cards . join ( '-' ) } < / div > ) ;

The cards come from the props of the hand. These props come from the component that contains the PokerHand .

In poker, cards belong to a player. Let's define the Player component:

const Player = (props) => ( <div className="player"> <div className="chips"> Chips: {props.chips} </div> <PokerHand cards={props.cards} /> </div> ); 1 2 3 4 5 6 7 8 9 10 const Player = ( props ) = > ( < div className = "player" > < div className = "chips" > Chips : { props . chips } < / div > < PokerHand cards = { props . cards } / > < / div > ) ;

The player component is included in a PokerGame component:

class PokerGame extends React.Component { state = { chips: 1200, cards: ['Ah', 'Kc'], board: null }; render() { return ( <Player cards={this.state.cards} chips={this.state.chips} /> ); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class PokerGame extends React . Component { state = { chips : 1200 , cards : [ 'Ah' , 'Kc' ] , board : null } ; render ( ) { return ( < Player cards = { this . state . cards } chips = { this . state . chips } / > ) ; } }

We can't pass the card data from the PokerGame component down to the PokerHand component directly, because PokerGame does not have direct access to the PokerHand . Data have to go through the Player component.

In a complex application, there are a lot of data, which have to be passed by components that have nothing to do with processing these data.

If you are new to React, you may want to know how to render the PokerGame component on screen. You can either use a boilerplate like create-react-app , or you can simply write a few lines of code to render your component on screen:

import ReactDOM from 'react-dom'; ReactDOM.render( <PokerGame />, document.querySelector('.js-app') ); 1 2 3 4 5 6 7 8 import ReactDOM from 'react-dom' ; ReactDOM . render ( < PokerGame / > , document . querySelector ( '.js-app' ) ) ;

Make sure to add a div with the class name js-app to the DOM to make this code work. If you need more information, check out my other React articles first.

Using the Context API

The Context API eliminates the need for prop drilling, in case you are not using Redux, MobX, or another complex framework.

A context is created by React.createContext() .

The Context API specifies two sub-components:

provider,

consumer.

The syntax for these two roles are as follows:

const ExampleContext = React.createContext(); // Provider: <ExampleContext.Provider /> // Consumer: <ExampleContext.Consumer /> 1 2 3 4 5 6 const ExampleContext = React . createContext ( ) ; // Provider: <ExampleContext.Provider /> // Consumer: <ExampleContext.Consumer />

A provider provides access to its data, while a consumer consumes it. Let's first create a context and a provider with the following steps:

Create a context using React.createContext() Create a context provider extending React.Component Move the state from the PokerGame component to the new context provider component Create a render method, where we wrap the rendered children of the element in a <PokerContext.Provider> component Add a value attribute to <PokerContext.Provider> , and equate it to this.state . This will provide access to the state stored by the context provider component.

const PokerContext = React.createContext(); class PokerContextProvider extends React.Component { state = { chips: 1200, cards: ['Ah', 'Kc'], board: null }; render() { return ( <PokerContext.Provider value={this.state}> {this.props.children} </PokerContext.Provider> ); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const PokerContext = React . createContext ( ) ; class PokerContextProvider extends React . Component { state = { chips : 1200 , cards : [ 'Ah' , 'Kc' ] , board : null } ; render ( ) { return ( < PokerContext . Provider value = { this . state } > { this . props . children } < / PokerContext . Provider > ) ; } }

The state of PokerContextProvider is made accessible anywhere inside its children, regardless of how deep the component hierarchy inside this.props.children is.

We can make the following changes to the PokerGame top level component:

Remove the state, as it is already in the provider. Wrap the contents returned by the render function with the provider. Stop passing down the state as props.

class PokerGame extends React.Component { render() { return ( <PokerContextProvider> <Player /> </PokerContextProvider> ); } } 1 2 3 4 5 6 7 8 9 10 11 class PokerGame extends React . Component { render ( ) { return ( < PokerContextProvider > < Player / > < / PokerContextProvider > ) ; } }

We will get rid of prop drilling in all children of PokerGame . Let's start with the Player class. As the Player is not receiving its props anymore, we have to get the chips from the context component by creating a consumer.

Let's recall the syntax of the provider:

// Provider: <ExampleContext.Provider value={this.state}> {this.props.children} </ExampleContext.Provider> 1 2 3 4 5 6 // Provider: < ExampleContext . Provider value = { this . state } > { this . props . children } < / ExampleContext . Provider >

The generic syntax of the consumer is as follows:

// Consumer: <ExampleContext.Consumer> {(contextValue) => ReactComponent} </ExampleContext.Consumer> 1 2 3 4 5 6 // Consumer: < ExampleContext . Consumer > { ( contextValue ) = > ReactComponent } < / ExampleContext . Consumer >

The consumer component contains a function that takes the context value and returns a React component. This React component typically contains values from contextValue , which gives you access to the state of the context provider component found in its value attribute. Note that this value does not necessarily have to be the state, it can be any valid React attribute value.

Now that we know the syntax of the consumer component, let's place it in the Player component to render the chip values:

const Player = (props) => ( <div className="player"> <div className="chips"> Chips: <PokerContext.Consumer> {(contextValue) => ( <strong>{contextValue.chips}</strong> )} </PokerContext.Consumer> </div> <PokerHand /> </div> ); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const Player = ( props ) = > ( < div className = "player" > < div className = "chips" > Chips : < PokerContext . Consumer > { ( contextValue ) = > ( < strong > { contextValue . chips } < / strong > ) } < / PokerContext . Consumer > < / div > < PokerHand / > < / div > ) ;

A <strong> HTML tag was added that makes the chip values appear in bold. This demonstrates that the return value is not necessarily a string, but an arbitrary React component.

Similarly, we can also create the PokerHand component without props, using another consumer:

const PokerHand = (props) => ( <div className="hand"> Hand: <PokerContext.Consumer> {(contextValue) => ( <strong>{contextValue.cards.join( '-' )}</strong> )} </PokerContext.Consumer> </div> ); 1 2 3 4 5 6 7 8 9 10 11 12 const PokerHand = ( props ) = > ( < div className = "hand" > Hand : < PokerContext . Consumer > { ( contextValue ) = > ( < strong > { contextValue . cards . join ( '-' ) } < / strong > ) } < / PokerContext . Consumer > < / div > ) ;

We are done with the rewrite. We don't need any props anymore, the Context API provides access to a simple store on top level.

Updating the State using the Context API

Updating the state of the context provider is a common task. The update action can be triggered from any child component that uses a context consumer. The implementation is straightforward. If you know the basics of React, you can already do it.

Let's add an all in feature to the Player component that changes your chip count to zero.

The first step is to pass down a function inside the value attribute of the provider that performs this action:

class PokerContextProvider extends React.Component { state = { chips: 1200, cards: ['Ah', 'Kc'], board: null }; render() { return ( <PokerContext.Provider value={{ state: this.state, allIn: () => this.setState({chips: 0}) }}> {this.props.children} </PokerContext.Provider> ); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class PokerContextProvider extends React . Component { state = { chips : 1200 , cards : [ 'Ah' , 'Kc' ] , board : null } ; render ( ) { return ( < PokerContext . Provider value = { { state : this . state , allIn : ( ) = > this . setState ( { chips : 0 } ) } } > { this . props . children } < / PokerContext . Provider > ) ; } }

Now that we reorganized the contents of the value attribute, we have to change all references in all providers to refer to the chips and cards state values as state.chips and state.cards respectively. You will see the corresponding changes in the code of the Player and the PokerHand classes.

Let's add an All In! button to the Player component that calls the allIn method.

const Player = (props) => ( <div className="player"> <div className="chips"> Chips: <PokerContext.Consumer> {(contextValue) => ( <strong> {contextValue.state.chips} <button onClick={contextValue.allIn}> All In! </button> </strong> )} </PokerContext.Consumer> </div> <PokerHand /> </div> ); const PokerHand = (props) => ( <div className="hand"> Hand: <PokerContext.Consumer> {(contextValue) => ( <strong>{contextValue.state.cards.join( '-' )}</strong> )} </PokerContext.Consumer> </div> ); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 const Player = ( props ) = > ( < div className = "player" > < div className = "chips" > Chips : < PokerContext . Consumer > { ( contextValue ) = > ( < strong > { contextValue . state . chips } < button onClick = { contextValue . allIn } > All In ! < /button> </s trong > ) } < / PokerContext . Consumer > < / div > < PokerHand / > < /div> ); const PokerHand = (props) => ( <div className="hand"> Hand: <PokerContext.Consumer> {(contextValue) => ( <strong>{contextValue.state.cards.join( '-' )}</s trong > ) } < / PokerContext . Consumer > < / div > ) ;

Once the allIn callback method is called, the setState method of the context provider is executed, changing the chip count to zero.

You can find the code belonging to this article on CodePen.

Summary

The Context API is geared towards smaller applications, where you need to have access to the state of a component more than one levels down the component hierarchy. The Context API may substitute a state management library like Redux, and your code stays maintainable as long as you create a small application.

As the application size grows, you may consider shifting towards proper application state management.