Context API: Making React’s state management effortless

Managing data in applications can be a great challenge, especially when many components are involved in sharing and updating that data. React components use states to manage properties internally, commonly being handled using React hooks such as useState or useEffect. However, things can get a little difficult when we must pass these properties from one component to another.

It is important to keep in mind that handling these states in large projects can be complicated. It feels like it’s almost mandatory to use third-party libraries to help with this task. And that’s exactly where Context API comes in handy.

What is the Context API and what is it good for?

First things first, Context is a built-in API recently introduced in React 16.3 that helps developers avoid passing data through a chain of props. Instead, the solution makes it possible to pass data from parent components to nested children directly, similar to Redux, but more lightly and easily.

In addition to being a way to create global variables that can be passed in, Context is a great alternative to avoid puncturing props. It is also very simple to use: you just got to create a context using React.createContext(), returning a Consumer and a Provider. 

For those unfamiliar with such names, Provider, as the term suggests, provides states to its children by becoming the parent of all components that might need those states. On the other hand, the Consumer is the component that consumes the states.

 

 Image 1: Example App.

To get a real understanding of the concept, this project exemplifies some of these context API applications. It consists of a simple score counter for two teams, and a result card, which shows the total score of each team.

In this app, the states we need to manage are one for each of the scores we want to keep track of: for two teams, we will need two different states. Other than that, the onClick functions will handle these states and return the sum of points. There are two ways to operate these states and it is important to know both to get a real sense of the trade-offs and when to use one instead of the other.

Prop drilling approach

The first one is by applying the prop drilling strategy: the required states for the components are passed through properties by the components themselves. The flowchart below is a good representation of how this is done. 

Here, states are created on the home page and sent, via props, to the Counter and Result components. These, in turn, send these states to other components that delve deeper and deeper into your application, causing the “drill” effect and creating nested components.

Image 2: Prop drilling approach flowchart.

See how our Home component is structured, initializing states and rendering the child components, Counter and Result.

export default function Home() {
  const [teamA, setTeamA] = useState(0);
  const [teamB, setTeamB] = useState(0);

  return (
    <Container>
      <Box>
        <Counter
          teamA={teamA}
          setTeamA={setTeamA}
          teamB={teamB}
          setTeamB={setTeamB}
        />
      </Box>
      <Result result={teamA + teamB} />
    </Container>
  );
}

What is the problem with Prop Drilling?

As mentioned before, Context helps us to avoid puncturing propellers. But why is it bad to use propeller drilling and what problems can this strategy cause? In our example, there is little problem in using helix drilling, since our application is still small and has a low amount of components and data interactions between them.

But what if we had a bigger system with more components and states to manage?

This would not only be a messy system to work with, but it would also certainly prove to be a headache to maintain.

We don’t even need a larger system to face problems: If you want to just rename the prop state you are working with, prop drilling will make you dig through every file and rename every state call individually.

While quite simple, the prop drilling approach will force you to pass data from the parent down to each level until you reach the desired child, even if you don’t need the data in these higher-level components. This will eventually get complicated and will be a pain to maintain.

Here at BIX Tech, one of the pillars of our software development practices is to deliver code that is easy to maintain even by third parties. This is why propeller drilling has no place in the systems developed by our teams.

At this point, you might be asking yourself: ok, so what’s the alternative? ‘Well, the solution and best option is the context API.

Getting our hands dirty with Context API

Our goal here is to provide data to the application wherever it’s needed so instead of passing the data to the higher-level component, we will provide the data on demand.

Image 3: How the Context API approach feeds the entire application with states.

To implement this we will utilize the Provider Pattern: It is nothing more than a High Order Component provided by a context object. That provider will receive a value prop that has the data we want to stream to the components. Every component that is wrapped on that provider will have access to the value.

Image 4: Comparison between the Prop Drilling approach and the Context API approach for the call flow of a state by a child component.

Now, how do use that data on each component?

Since we are on React, hooks are the way to go: Import the useContext hook, which receives the context that has the value prop we created. This will allow us to read and write data to the Context.

By doing that, the components that are not using the data won’t have to deal with it and the data is called only when and where needed, in the correct scope.

This approach cannot be compared to prop drilling, can it?

Understand the approach in practice with examples

Let’s go back to the Scoreboard Case. We first build an App Provider. It will wrap the root of our app in the App Provider component. 

As you may have noticed, this is not the context where the fun happens, right? We can, and sometimes should, have more than one context in our applications. That will help to build a modular solution, making further development and maintenance easy.

App

import AppProvider from "./contexts";
import Home from "./pages/Home";

function App() {
  return (
    <AppProvider>
        <Home />
    </AppProvider>
  );
}

export default App;
 

App Provider

import React from 'react'

import { ScoreProvider } from './ScoreContext'

const AppProvider = ({ children }) => (
      <ScoreProvider>{children}</ScoreProvider>
)

export default AppProvider

After that, by utilizing createContext() we will be able to create the ScoreProvider. This is the key element of the Context API approach. 

Here we will declare every state that we need, apply handlers as functions, and of course, export everything as an object so that we can import individual props on each component.

Score Provider

import React, {
  createContext,
  useState,
} from 'react'

export const ScoreContext = createContext({})

const ScoreProvider = ({ children }) => {
  const [teamA, setTeamA] = useState(0);
  const [teamB, setTeamB] = useState(0);

  function handleIncreaseTeamAScore(){
      setTeamA((prevScore) => prevScore+1)
  }

  function handleIncreaseTeamBScore(){
      setTeamB((prevScore) => prevScore+1)
  }

  function handleDecreaseTeamAScore(){
    setTeamA((prevScore) => prevScore-1)
}

  function handleDecreaseTeamBScore(){
      setTeamB((prevScore) => prevScore-1)
  }

  function getScoreSum(){
      return teamA+teamB
  }

  return (
    <ScoreContext.Provider
      value={{
        teamA,
        setTeamA,
        teamB,
        setTeamB,
        handleIncreaseTeamAScore,
        handleIncreaseTeamBScore,
        handleDecreaseTeamAScore,
        handleDecreaseTeamBScore,
        getScoreSum
      }}
    >
      {children}
    </ScoreContext.Provider>
  )
}

export { ScoreProvider }

Another smart thing to do in this approach is to create a hook that calls the context for us. This helps a lot in building a modular, clean code.

useScore hook

import { useContext } from "react"
import { ScoreContext } from "../contexts/ScoreContext"

function useScore() {
    const context = useContext(ScoreContext)

    if (!context) {
      throw new Error('useScore must be used within a ScoreProvider')
    }

    return context
  }

  export { useScore }

All that is done so that in the end our components can be transformed from what we saw earlier, to this below. Note how we only call what we need on each of them.

Home page

export default function Home() {
  return (
    <Container>
      <Box>
        <Counter />
      </Box>
      <Result />
    </Container>
  );
}
 

Result Component

export function Result() {
  const { getScoreSum } = useScore();
  return (
    <Box>
      <Paper>
        <h1>Sum of score</h1>
        <h2>{getScoreSum()}</h2>
      </Paper>
    </Box>
  );
}
 

Counter Component

export function Counter() {
  return (
    <Box>
      <TeamA />
      <TeamB />
    </Box>
  );
}
 

TeamA Component

export function TeamA() {
  const { teamA, handleIncreaseTeamAScore, handleDecreaseTeamAScore } =
    useScore();

  return (
    <Paper>
      <Box>
        <h2>Team A</h2>
      </Box>
      <Scoreboard
        score={teamA}
        handleIncreaseScore={handleIncreaseTeamAScore}
        handleDecreaseScore={handleDecreaseTeamAScore}
      />
    </Paper>
  );
}
 

ScoreBoard Component

export function Scoreboard({
  score,
  handleIncreaseScore,
  handleDecreaseScore,
}) {
  return (
    <Paper>
      <h2>Score Board</h2>
      <Stack>
        <Button onClick={handleIncreaseScore}>Score +</Button>
        <Button onClick={handleDecreaseScore}>Score - </Button>
      </Stack>
      <h2>{score}</h2>
    </Paper>
  );
}
 

Wrapping things up

Is important to mention there are also alternatives to the React Context API. It can all depend on the size of your application, dependencies, and complexity but alternatives like Redux, Hookstate or Recoil are very popular and should also be considered. Each one of them will have its pros and cons. However, a team of experts at BIX Tech found that the Context API works very well in most cases.

React’s Context API lays out the basis for avoiding prop drilling and after you start using it, it’s hard to go back to prop drilling. We recommend that you implement it in your projects, find its limitations and explore other options as we mentioned. Have fun doing it and of course, you can always get in touch with us via our LinkedIn page.

If you want a deeper look at the example we built, it’s stored on our Github. You can access the repository by clicking here.

Also, if you want to get in touch with our consultants who wrote this article, feel free to reach out to LucasPedro, and Thiago on their LinkedIn profiles.

Don't miss any of our content

Sign up for our BIX News

Our Social Media

Most Popular