How To Use The React Context API (Pre-Hooks)
By: Luke Ghenco
Tags: JavaScript, Frontend, React Context Api, Reactjs, React

Reacting to Context

If you’ve recently been hearing a lot about the new React Context API and wanted to know more about it, this post is for you. This article will show some examples of how to use React context for managing shareable state and actions while avoiding the dreadful issues related to “Prop Drilling” in a React app.

This post is a continuation of my React series, so be sure to check out my other React posts (Creating Readable Tests Using React Testing Library & Testing Async React Hooks).

DISCLAMER!

This is not written using a version of React that had hooks. This implementation still should work with versions of React that have hooks, but if context is attached to a hook it might behave differently then expected

Building an Account Profile App

In the following walkthrough, we are going to be building a simple Account Profile app. Something like this:

Initial Component and File structure

Using the above mock, you can see that we have 3 parts to the app:

A navigation bar with a link to a home page and the account’s username A detail component that displays the account info A form to create a new username or change the membership level of an account

To follow along with the code in this app, run the following commands to bootstrap the project.

$ npx create-react-app account-profile-app

$ cd account-profile-app

$ yarn add react-router-dom // or npm install react-router-dom

The first thing we need to do is lay out the foundational component structure of the app. We will then connect the components to an AccountProvider component created with the Context API.

Setup and create the necessary files to make the following file structure:

| src

| NavigationBar.jsx

| AccountProfile

index.jsx

AccountDetails.jsx

AccountUpdate.jsx

App.jsx

index.js

For the navigation bar, we need to create a stateless component called NavigationBar. This will display links to the home page and the account profile page.

// NavigationBar.jsx

import React from 'react'

import { Link } from 'react-router-dom' const NavigationBar = ({ username }) => (

<div>

<Link to="/">Home</Link>

<Link to="/account/profile">{username}</Link>

</div>

) export default NavigationBar

Now, let’s build the AccountDetails component that will display the account’s username, date joined, and membership level.

// AccountProfile/AccountDetails.jsx

import React from 'react' const AccountDetails = ({ username, dateJoined, membershipLevel }) => (

<div>

<p>Username: {username}</p>

<p>Date Joined: {dateJoined}</p>

<p>Membership Level: {membershipLevel}</p>

</div>

) export default AccountDetails

Next, we can create the AccountUpdate component that will be a basic form allowing the creation of a new username or change a membership level. This component will also need a small amount of internal state to handle on change and form submission events.

// AccountProfile/AccountUpdate.jsx

import React from 'react' class AccountUpdate extends Component {

state = {

username: this.props.username,

membershipLevel: this.props.membershipLevel

} handleOnChange = ({ target: { value, name }}) => {

this.setState({

[name]: value

})

} render () {

const { membershipLevel, username } = this.state



return (

<div>

<form>

<label htmlFor="username">New Username</label>

<div>

<input

type="text"

name="username"

value={usernameValue}

onChange={this.handleOnChange}

/> </div>

<label htmlFor="membershipLevel">Membership Level</label>

<div>

<select

value={membershipLevel}

name="membershipLevel"

onChange={this.handleOnChange}

>

<option value=”Bronze”>Bronze</option>

<option value=”Silver”>Silver</option>

<option value=”Gold”>Gold</option>

</select>

</div>

<button>Save</button

</form>

</div>

)

}

} export default AccountUpdate

Now that the the initial AccountUpdate and AccountInfo components are built, we should place them inside of an AccountProfile component.

// AccountProfile/index.jsx



import AccountDetails from './AccountDetails'

import AccountUpdate from './AccountUpdate' const AccountProfile = ({ account: { username, dateJoined, membershipLevel } }) => (

<Fragment>

<AccountDetails username={username} dateJoined={dateJoined} membershipLevel={membershipLevel} />

<AccountUpdate username={username} membershipLevel={membershipLevel} />

</Fragment>

) export default AccountProfile

Finally, we can mount the components we created to the App component wrapped inside a Router component. We also need to create an account object that will be our temporary state holder for account info until we add in the AccountProvider.

// App.jsx

import React from 'react'

import {

BrowserRouter as Router,

Route,

Switch

} from 'react-router-dom'

import NavigationBar from './NavigationBar'

import AccountProfile from './AccountProfile' const account = {

username: 'Crunchy Crunch',

dateJoined: '9/1/18',

membershipLevel: 'Silver'

} const App = () => (

<Router>

<Fragment>

<NavigationBar username={username} />

<Switch>

<Route

exact

path=”/”

render={() => <div>Home</div>}

/>

<Route

exact

path=”/account/profile”

render={() => <AccountProfile account={account} />}

/>

</Switch>

</Fragment>

</Router>

) export default App

Adding Context to our App

With the initial components we created, the account data for the NavigationBar and AccountProfile was hard-coded in the App component and then passed down as props to the other components. The passing down of props is not out of hand yet, but this could be troublesome as our app grows.

Currently, we are passing down props about the account from the App component to the child NavigationBar component. We are also passing down props from the App component to the child AccountProfile component, which then passes props down to its children. This is where passing props is starting to become a problem in our app and a typical case of “Props Drilling”, as mentioned earlier. We want to make our app more flexible and not have to adjust code in so many places during a refactor to avoid brittle code. This is where a solution like Redux or MobX might have been used, but now we can use React Context to simplify this problem for us.

Our solution for this will be to to set up a data provider using the context API. This will allow us to share state with our components, while using a Singleton Design Pattern for our data store that all of our components can listen / subscribe too.

So for the next step, let’s set up an AccountProvider and an AccountConsumer by creating a new context object and React component that contains local state. We will later integrate this data provider with our components to get dynamic data.

// src/providers/AccountProvider.js

import React from 'react' // Set Up The Initial Context

const AccountContext = React.createContext() // Create an exportable consumer that can be injected into components

export const AccountConsumer = AccountContext.Consumer // Create the provider using a traditional React.Component class

class AccountProvider extends Component {

state = {

username: 'Crunchy Crunch',

dateJoined: '9/1/18',

membershipLevel: 'Silver'

} render () {

return (

// value prop is where we define what values

// that are accessible to consumer components

<AccountContext.Provider value={this.state}>

{this.props.children}

</AccountContext.Provider>

)

}

} export default AccountProvider

Now that we’ve created the AccountProvider & Account Consumer, we can connect / render it on the top level of our App component. This will make the AccountProvider values accessible to all children components. We will also remove our temporary account object, and its use as a prop passed to the NavigationBar and AccountProfile components.

// App.jsx — update ... previous imports

import AccountProvider from ‘./providers/AccountProvider’ // remove const account const App = () => (

<AccountProvider>

<Router>

<Fragment>

<NavigationBar />

<Switch>

<Route

exact

path=”/”

render={() => <div>Home</div>}

/>

<Route

exact

path=”/account/profile”

component={AccountProfile}

/>

</Switch>

</Fragment>

</Router>

</AccountProvider>

)

Let’s talk about what we’ve completed so far:

Setup our components Created an AccountProvider using the context API Created an AccountConsumer that will be used by child components to access values from the AccountProvider Mounted the AccountProvider as the top level component in our application Removed our temporary account object and simplified our props

Everything is in good shape so far, but how do we make our components subscribe to our AccountProvider? Let’s take a shot with the NavigationBar first.

// NavigationBar.jsx — Update ...previous imports

import { AccountConsumer } from '../providers/AccountProvider' const NavigationBar = (_props) => (

<AccountConsumer>

{({ details: { username } }) => (

<div>

<Link to=”/”>Home</Link>

<Link to=”/account/profile”>{username}</Link>

</div>

)}

</AccountConsumer>

) export default NavigationBar

Ok, so that was a lot. Let’s do one more recap of our accomplishments:

We imported the AccountConsumer We rendered the AccountConsumer as the top level component of the NavigationBar We defined a callback block that passed the username value from the local state of the AccountProvider We rendered the original code of the NavigationBar component that now uses the new username prop given by the AccountConsumer

This made the NavigationBar an active subscriber of the AccountProvider. If the username state is changed in the AccountProvider, it will now render that new value in the NavigationBar.

Next, let’s update the AccountDetails, using the same pattern, but passing the values username , membershipLevel, and dateJoined .

// AccountProfile/AccountDetails.jsx — Update ...previous imports

import { AccountConsumer } from '../providers/AccountProvider' const AccountDetails = () => (

<AccountConsumer>

{({ username, dateJoined, membershipLevel }) => (

<div>

<p>Username: {username}</p>

<p>Date Joined: {dateJoined}</p>

<p>Membership Level: {membershipLevel}</p>

</div>

)}

</AccountConsumer>

) export default AccountDetails

Awesome job! We are showing data defined in the AccountProvider inside of our NavigationBar & AccountDetails components.

We just need to make one adjustment to the AccountProfile component to remove unused prop drilling now.

// AccountProfile/index.jsx - Update



import AccountDetails from ‘./AccountDetails’

import AccountUpdate from ‘./AccountUpdate’ const AccountProfile = () => (

<Fragment>

<AccountDetails />

<AccountUpdate />

</Fragment>

) export default AccountProfile

Yay, we are at the halfway point of the application. Feel free to take a break and read up on passing props from context providers to children components (I highly recommend “React’s New Context API”). Continue on to see how to dynamically update state in our AccountProvider.

Dynamically Updating Provider Context State

To complete the following task, we will need to create a new function in our AccountProvider that can update the local state for our subscribed components to receive.

Let’s call this function updateAccount() and then bind it to the state object.

// AccountProvider.jsx — update

import React from 'react' const AccountContext = React.createContext() export const AccountConsumer = AccountContext.Consumer class AccountProvider extends Component {

state = {

username: ‘Crunchy Crunch’,

dateJoined: ‘9/1/18’,

membershipLevel: ‘Silver’,

updateAccount: updatedAccount => this.updateAccount(updatedAccount)

}



updateAccount = updatedAccount => {

this.setState(prevState => ({

…prevState,

…updatedAccount

}))

}



render () {

return (

<AccountContext.Provider value={this.state}>

{this.props.children}

</AccountContext.Provider>

)

}

} export default AccountProvider

With this newly created code in the AccountProvider, we’ve added:

An updateAccount key to the local state that references a bound updateAccount() function An updateAccount() function that updates the local state of the AccountProvider Sets updateAccount() as a passable function to subscribers

With this new function available to us, we can now update the AccountUpdate component’s form to use it. Let’s place it inside of a handleOnSubmit function and pass it the update account information from our form. We should also pass down the username and membership level from the AccountProvider.

// AccountProfile/AccountUpdate.jsx — update import React from 'react'

import { AccountConsumer } from '../providers/AccountProvider' class AccountUpdate extends Component {

// Updates!!

state = {

username: this.props.username,

membershipLevel: this.props.membershipLevel

} handleOnChange = ({ target: { value, name }}) => {

this.setState({

[name]: value

})

} // New Code!!

handleOnSubmit = event => {

event.preventDefault() const updatedAccount = { ...this.state } this.props.updateAccount(updatedAccount)

} // New Code!!

// To handle resetting form upon submission success

componentWillReceiveProps(nextProps, prevProps) {

if(prevProps !== nextProps) {

this.setState({

username: nextProps.username,

membershipLevel: nextProps.membershipLevel

})

}

} // Updates!!

render () {

const { membershipLevel, username } = this.state

const usernameValue =

username === this.props.username ? '' : username return (

<div>

<form onSubmit={this.handleSubmit}>

<label htmlFor=”username”>New Username</label>

<div>

<input

type=”text”

name=”username”

value={usernameValue}

onChange={this.handleOnChange}

/>

</div>

<label htmlFor=”membershipLevel”>Membership Level</label>

<div>

<select

value={membershipLevel}

name=”membershipLevel”

onChange={this.handleOnChange}

>

<option value=”Bronze”>Bronze</option>

<option value=”Silver”>Silver</option>

<option value=”Gold”>Gold</option>

</select>

</div>

<button>Save</button

</form>

</div>

)

}

} // Added to connect AccountConsumer

// To pass props to AccountUpdate

// Before component initialization

// As the AccountUpdate.state requires

// The new props

const ConnectedAccountUpdate = props => (

<AccountConsumer>

{({ username, membershipLevel, updateAccount }) => (

<AccountUpdate

{...props}

username={username}

membershipLevel={membershipLevel}

updateAccount={updateAccount}

/>

)}

</AccountConsumer>

) export default ConnectedAccountUpdate

Voilà! On form submission, we are now updating the username and membership level dynamically, which, now appears with the new changes in the NavigationBar and AccountDetails components.

Conclusion:

I hope this article provides you with some ideas on how you can use it in your current projects. I’ve enjoyed my time working with the new tool, and appreciate having even more control of my React code.

Resources: