Digging Into React Context

Avatar of Kingsley Silas
Kingsley Silas on

📣 Freelancers, Developers, and Part-Time Agency Owners: Kickstart Your Own Digital Agency with UACADEMY Launch by UGURUS 📣

You may have wondered lately what all the buzz is about Context and what it might mean for you and your React sites. Before Context, when the management of state gets complicated beyond the functionality of setState, you likely had to make use of a third party library. Thanks to recent updates by the awesome React team, we now have Context which might help with some state management issues.

What Does Context Solve?

How do you move the state from a parent component to a child component that is nested in the component tree? You know that you can use Redux to manage state, but you shouldn’t have to jump to Redux in every situation.

There’s a way to do this without Redux or any other third party state management tool. You can use props!

Say the feature you want to implement has a tree structure similar to what I have below:

The state lives in the App component and is needed in UserProfile and UserDetails components. You need to pass it via props down the tree. If the components that need this state are 10 steps deep, this can become tedious, tiring, and error prone. Each component is supposed to be like a black box — other components should not be aware of states that they do not need. Here is an example of an application that matches the scenario above.

class App extends React.Component {
  state = {
    user: {
      username: 'jioke',
      firstName: 'Kingsley',
      lastName: 'Silas'
    }
  }

  render() {
    return(
      <div>
        <User user={this.state.user} />
      </div>
    )
  }
}

const User = (props) => (
  <div>
    <UserProfile {...props.user} />
  </div>
)

const UserProfile = (props) => (
  <div>
    <h2>Profile Page of {props.username}</h2>
    <UserDetails {...props} />
  </div>
)

const UserDetails = (props) => (
  <div>
    <p>Username: {props.username}</p>
    <p>First Name: {props.firstName}</p>
    <p>Last Name: {props.lastName}</p>
  </div>
)

ReactDOM.render(<App />, document.getElementById("root"));

We are passing the state from one component to another using props. The User component has no need of the state, but it has to receive it via props in order for it to get down the tree. This is exactly what we want to avoid.

See the Pen React Context API Pen 1 by Kingsley Silas Chijioke (@kinsomicrote) on CodePen.

Context to the Rescue!

React’s Context API allows you to store the state in what looks like an application global state and access it only in the components that need them, without having to drill it down via props.

We start by initializing a new Context using React’s createContext()

const UserContext = React.createContext({})
const UserProvider = UserContext.Provider
const UserConsumer = UserContext.Consumer

This new Context is assigned to a const variable, in this case, the variable is UserContext. You can see that there is no need to install a library now that createContext() is available in React (16.3.0 and above).

The Provider component makes the context available to components that need it, which are called Subscribers. In other words, the Provider component allows Consumers to subscribe to changes in context. Remember that the context is similar to a global application state. Thus, components that are not Consumers will not be subscribed to the context.

If you are coding locally, your context file will look like this:

import { createContext } from 'react'

const UserContext = createContext({})
export const UserProvider = UserContext.Provider
export const UserConsumer = UserContext.Consumer

The Provider

We’ll make use of the Provider in our parent component, where we have our state.

class App extends React.Component {
  state = {
    user: {
      username: 'jioke',
      firstName: 'Kingsley',
      lastName: 'Silas'
    }
  }

  render() {
    return(
      <div>
        <UserProvider value={this.state.user}>
          <User />
        </UserProvider>
      </div>
    )
  }
}

The Provider accepts a value prop to be passed to it Consumer components descendants. In this case, we will be passing the user state to the Consumer components. You can see that we are not passing the state to User component as props. That means we can edit the User component and exclude the props since it does not need them:

const User = () => (
  <div>
    <UserProfile />
  </div>
)

The Consumer

Multiple components can subscribe to one Provider component. Our UserProfile component needs to make use of the context, so it will subscribe to it.

const UserProfile = (props) => (
  <UserConsumer>
    {context => {
      return(
        <div>
          <h2>Profile Page of {context.username}</h2>
          <UserDetails />
        </div>
      )
    }}
  </UserConsumer>
)

The data we injected into the Provider via the value prop is then made available in the context parameter of the function. We can now use this access the username of the user in our component.

The UserDetails component will look similar to the UserProfile component since it is subscriber to the same Provider:

const UserDetails = () => (
  <div>
    <UserConsumer>
      {context => {
        return (
          <div>
            <p>Userame: {context.username}</p>
            <p>First Name: {context.firstName}</p>
            <p>Last Name: {context.lastName}</p>
          </div>
        )
      }}
    </UserConsumer>
  </div>
)

See the Pen React Context API Pen 2 by Kingsley Silas Chijioke (@kinsomicrote) on CodePen.

Updating State

What if we want to allow users to change their first and last name? That’s also possible. Consumer components can re-render whenever there are changes to the value passed by the Provider component. Let’s see an example.

We’ll have two input fields for the first and last name in the consumer component. From the Provider component, we will have two methods that update the state of the application using the values entered in the input fields. Enough talk, let’s code!

Our App component will look like this:

class App extends React.Component {
  state = {
    user: {
      username: 'jioke',
      firstName: 'Kingsley',
      lastName: 'Silas'
    }
  } 

  render() {
    return(
      <div>
        <UserProvider value={
          {
            state: this.state.user,
            actions: {
              handleFirstNameChange: event => {
                const value = event.target.value
                this.setState(prevState => ({
                  user: {
                    ...prevState.user,
                    firstName: value
                  }
                }))
              },

              handleLastNameChange: event => {
                const value = event.target.value
                this.setState(prevState => ({
                  user: {
                    ...prevState.user,
                    lastName: value
                  }
                }))
              }
            }
          }
        }>
          <User />
        </UserProvider>
      </div>
    )
  }
}

We are passing an object which contains state and actions to the value props which the Provider receives. The actions are methods that will be triggered when an onChange event happens. The value of the event is then used to update the state. Since we want to update either the first name or last name, there’s a need to preserve the value of the other. For this, we make use of ES6 Spread Operator, which allows us to update the value of the specified key.

With the new changes, we need to update UserProfile component.

const UserProfile = (props) => (
  <UserConsumer>
    {({state}) => {
      return(
        <div>
          <h2>Profile Page of {state.username}</h2>
          <UserDetails />
        </div>
      )
    }}
  </UserConsumer>
)

We use ES6 destructuring to extract state from the value received from the Provider.

For the UserDetails component, we both the state and actions. We also need to add two input fields that will listen for an onChange() event and call the corresponding methods.

const UserDetails = () => {
  return (
    <div>
      <UserConsumer>
        {({ state, actions }) => {
          return (
            <div>
              <div>
                <p>Userame: {state.username}</p>
                <p>First Name: {state.firstName}</p>
                <p>Last Name: {state.lastName}</p>
              </div>
              <div>
                <div>
                  <input type="text" value={state.firstName} onChange={actions.handleFirstNameChange} />
                </div>
                <div>
                  <input type="text" value={state.lastName} onChange={actions.handleLastNameChange} />
                </div>
              </div>
            </div>
          )
        }}
      </UserConsumer>
    </div>
  )
}

Using Default Values

It is possible to pass default values while initializing Context. To do this, instead of passing an empty object to createContext(), we will pass some data.

const UserContext = React.createContext({
  username: 'johndoe',
  firstName: 'John',
  lastName: 'Doe'
})

To make use of this data in our application tree, we have to remove the provider from the tree. So our App component will look like this.

class App extends React.Component {
  state = {
    user: {
      username: 'jioke',
      firstName: 'Kingsley',
      lastName: 'Silas'
    }
  }

  render() {
    return(
      <div>
        <User />
      </div>
    )
  }
}

See the Pen React Context API Pen 4 by Kingsley Silas Chijioke (@kinsomicrote) on CodePen.

The data that will be used in the Consumer components will be done defined when we initialized a new Context.

In Conclusion

When things get complicated, and you are tempted to run yarn install [<insert third-party library for state management], pause for a second — you’ve got React Context at the ready. Don’t you believe me? Maybe you’ll believe Kent C. Dodds.