How to use React Context with useState

Korbinian Schleifer
comsystoreply
Published in
7 min readOct 12, 2022

Are you tired of using props in your React App everywhere? Are you searching for an alternative to Redux? Are your child components passing props further down without using them? Here is a better way!

React Context plus useState with the React logo

Prerequisites: Basic React knowledge about props, state, components, and hooks. Additionally some TypeScript knowledge. We will use functional components in this example. (Also check out the article from my colleague Johannes on Considerations for writing React components)

Outline

1. The problem and when to use Context
2. The Basics with an easy Implementation
3. A more realistic example
4. What to consider

1. The Problem and when to use Context

One of the most common problems or considerations for using Context is the prop drilling problem. To illustrate this problem we will use a food ordering app as an example. So just imagine you wrote your fancy new food ordering app. Your app has food items that you want to display in a list. Each food item has a counter where you can add items to the shopping cart. To display the items before the order you also wrote a shopping cart component that displays the quantity of the food with a FoodSummary component. The component tree of the app might look something like this:

Food Ordering App component tree with sub components. Food Item and Shopping Cart are sub components of App. Food Counter is a sub component of Food Item. Food Summary is a sub component of Shopping Cart.

Here is a simplified code example of how we might keep track of the current count of a food item. The counter state is at the top of the tree because we need it for the FoodCounter and the FoodSummary component.

As you can see we are passing the state down as prop through the whole tree. In the FoodItem component, we might not even need the state but we only pass it through to the FoodCounter component. This is what the prop drilling problem is all about. (A similar thing could happen with the ShoppingCart and FoodSummary components, which are not shown in the code example)

By this, we congest our components and it might get hard for other people reading our code to figure out what is going on in them. This might even get worse when we start passing down multiple properties. But what we want is to keep our components clean and only have the states where we actually need them. This is where React Context comes to the rescue.

2. The Basics with an easy Implementation

Now let’s put this example into perspective. We will rename all building blocks to Component so we can focus on the important part: building up our Context. We will create a Context, a Context Provider, and a Context Consumer to manage our state in a different way.

The three main building blocks

React component tree with a context that is on the side. The Provider is at the parent component and the consumer is at the child component.

As you can see from the image above, the three main building blocks are the Context, the Provider, and the Consumer so we need to:

  1. Create the Context
  2. Wrap the parent component with the Provider
  3. Consume the Context

1. Create the Context

As you can see in the code example, it is really easy to create a Context in React. You just need to call the createContext function. As an argument, you can pass an optional default value. In this case, we just pass "defaultValue" as a string.

2. Wrap the parent component with the Provider

In the parent component, we can wrap the elements that we return with our Context. Specifically, we are wrapping MyParentComponent with the Provider property of MyContext. This is a default property we get from React after the Context is created. If the Context is created in another file we can easily import it here. As you can see from the example the Provider can be used like any regular component in React.

Additionally, the Context Provider component comes with a value prop. We can use this to pass any value we like. In this case, we just create a "My Context Value" string and pass it to the Context Provider.

3. Consume the Context and use the value

Inside MyComponent or some child component down the tree we can use:

So now we can consume our Context directly in MyComponent or in any of its child components. To consume the Context we just call the useContext Hook and pass it our Context (MyContext) as an argument. This gives us the value we provided in the Provider. I guess you can see now where the names are coming from 😉.

When we have the value we can use it like any other kind of prop or state variable. Here we just return it inside a paragraph.

Important: How does React know which value to use?

React sees that you are trying to use your Context in the consumer. To find the corresponding value of the Context, React goes up the component tree and looks for a matching provider. If it finds the provider, it uses the value that was defined there. If React however does not find a matching provider going up the component tree, then it will use the default value that you provided on Context creation.

3. A more realistic example

The first examples were generic and minimalistic to explain the concept of React Context. Now we will look at an example that is much closer to what you would find in a real-world application. As an example, we will use an onboarding or registration flow, where a user signs up in our app.

For this example, we will use TypeScript. Also, we will create our own Provider component where we will use the useState hook. Imagine that the onboarding flow has multiple steps where we want to share the data between these steps and also show the data in a summary screen. Because we don’t want to pass all the data around as props we will use an OnboardingContext.

As a starting point, we create an interface for the Context with the username state and a setUsername state action. For simplification, we only use the username here. But of course, you could extend the interface with more properties like the mail address or the user's birthday.

After that, we just create our Context as before, which can be of type OnboardingContextValue or undefined. As a default value, we pass undefined. Now let’s create our custom Provider component.

Here we just call our usual useState hook and unpack the username and the setUsername function. Then we return our onboarding Context Provider with all child components. As values, we just pass our username and the setUsername function. Of course, at this point, you could also put in more states and pass them with the value prop to the Provider. Maybe you already wondered why we passed undefined as a default value on our Context creation. Here comes the answer:

We wrap our Onboarding Context consumer in our own custom hook. This is a nice trick that helps us to catch errors.

When the Context is created and can’t find a matching provider, undefined is used as a default value. This will throw an error for us. We actually want this error to happen, because we don’t want to use the default value with our Context. We want to use the states that we defined in our custom Provider component. But if the onboardingContext is undefined we have probably forgotten to wrap our parent component with our custom Provider.

In order to solve this issue we wrap our parent component with our custom Provider:

To use our state values we can use our custom hook in one of the child components. We just unpack the username and the setUsername function. Now we can use them like any other state value or state action. Similar like when we would have used the useState hook in this component directly. This also makes it easy to move states from single components to a Context.

4. What to consider

There are certain things that you should consider before introducing Context into your codebase. First, you should start thinking about your data. What data do you need for your components? And also: Where exactly is this data needed? This should give you a clearer picture of your whole application in general. This also includes which data fits together and if the data should really be in a shared Context.

After that, you can continue by asking yourself: What states am I currently using? You might still need these states but can move them to the Context. Ask yourself which states you want to have in your Context and which state is not needed there, because it is only needed for one component. Also, ask yourself how you are changing the state. You can move the set state functions to your Context but also go a step further by moving custom handlers there.

Lastly, you should ask yourself if prop drilling is really such a big problem for you. Creating a Context comes with a particular overhead. It might make your application harder to change or refactor. For example Context makes it harder to test your components. It is easier to pass mocked props to your component than writing a custom test render function that wraps the component with the Context in the test. Maybe the overhead of the Context is not worth the small annoyance of passing props.

Also if we use Context it can be hard to understand how data flows between components. If we use props, it is always obvious, what data goes in and out of a component. With Context would need to do some digging first to find out where the data is actually coming from.

To summarise

> Not everything needs to be in one Context.

> Your Context does not need to be globally accessible. Keep state as close to where it is needed as possible.

> Is prop drilling really a problem in you application?

Do you like what we are doing and do you want to become part of it? Check out our current job offers: https://www.comsystoreply.com/career

This blogpost is published by Comsysto Reply GmbH

--

--