How To Use The React Context API
How To Use The React Context API
Reacting to Context
If you’ve recently been hearing a lot about the new React Context APIand 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 “
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.jsximport 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.jsximport 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.jsximport 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.jsximport 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 componentwrapped 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.jsximport React from 'react'import { BrowserRouter as Router, Link } 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 componentto the child NavigationBar component. We are also passing down props from the App componentto 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 creatinga 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.jsimport React from 'react'
// Set Up The Initial Contextconst AccountContext = React.createContext()
// Create an exportable consumer that can be injected into componentsexport const AccountConsumer = AccountContext.Consumer
// Create the provider using a traditional React.Component classclass 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 valuesaccessible 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 importsimport 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 importsimport { 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 thesame pattern, but passing the values username
, membershipLevel,
anddateJoined
.
// 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 - Updateimport 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 — updateimport 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 boundupdateAccount()
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 propsconst 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.