React Contexts

Chapter 29 42 mins

Learning outcomes:

  1. What are contexts
  2. Creating a context using createContext()
  3. Creating a provider using the Provider component
  4. Consuming a context using useContext()
  5. Conventional way of working with contexts in React
  6. Preventing re-rendering everything upon a context's value change
  7. Internals of contexts
  8. Default context value

Introduction

Let's say you're finally done creating an app in React and right before closing the computer, get to know that you have to pass in an extra piece of state data from the top-most App component down to a deeply nested component. What would you do?

Or what if you receive a call from a co-developer working on the app with you, asking you to add in support for i18n (a short form for 'internationalization') by providing an additional lang prop to any component that needs to leverage i18n. Would you go on and change the definition of every single component to entertain this new prop?

No, right? You're burnt out from working on the app since a long time. You're not in the mood to be sitting in front of the screen for implementing something which requires a vast amount of repetition. Programmers are extremely lazy (in a good way), remember?

Well, enter contexts in React.

Using contexts, you can easily pass in the state data from the top-level App component down to a deeply nested component, or provide the lang prop to every component that needs i18n, literally in the matter of a couple of minutes, without needing to use props at every stage in the component hierarchy.

In this chapter, we shall understand what exactly is context in React, starting with understanding why exactly do we even need to learn it in the first place.

As with many shiny features of React, context is an opt-in feature, likewise we'll understand when to avoid using it as well. We'll see the createContext() function exported by react to create a context, the Provider component of a context, and also the useContext() hook meant for consuming a given context.

We'll also see how to optimize a workflow based on contexts by refactoring code in a way wherein state changes happen in a way such that React doesn't need to render everything all over again but just the components that consume a component.

There's honestly a ton of information to cover in this chapter — the characteristics of a pro React developer — and so you should make sure to go slow and comprehend everything to the full.

Without expending any more of our time, it's time to get learning…

What are contexts?

Context isn't a hard-to-understand concept in React; it could be well understood in the matter of 5 mins. Yes, that's right — 5.

In short,

Context is a way to pass down data from a top-level component to a bottom-level component without using props.

And that's essentially it. Context in React is merely a 'prop-less' approach to passing data between components.

Let's dive a little deeper into the backstory of this approach…

As you'd recall from React Components: Prop drilling, passing data from a top-level component to a bottom-level component in React basically means that we have to use props carrying that data.

The idea is that props are provided by the top-level component to its respective children, which then pass them on down to their children, which pass them on to their children, and this keeps on happening until the props eventually reach the bottom-level component. This approach is commonly known as prop drilling.

Prop drilling isn't a bad approach, per se. In fact, it's a great way to know which component requires which piece of data for its internal operations. It's always good to know which data is consumed by which component just by judging the code.

However, prop drilling does turn out to be quite strenuous when the chain of components that need to merely delegate some props (they won't ever use themselves) becomes long enough.

For instance, suppose that we have the following tree of components, starting at the root-level component, App:

The component tree of a hypothetical React app
The component tree of a hypothetical React app

We need to pass on some state data managed by App three levels down to the component UserInfo.

Using prop drilling for this means that we need to let the data pass through every single component in between: Sidebar and Header in this case.

Using prop drilling to pass data down from App to Button
Using prop drilling to pass data down from App to Button

Here the chain of elements merely consuming the data for the purpose of forwarding it to their respective child has 2 elements (Sidebar and Header). UserInfo obviously needs the data, hence we don't count it in this chain.

In small apps with such small chains, prop drilling might be just fine. But with longer chains, we definitely seek some way to get out of this dilemma. We can't be passing one prop down a hundred components, with not even a single one of them using it for itself!

And that's precisely where context comes into the game.

It allows us to pass data from App down to Button without ever using props. It allows data to be managed from a top-level component in kind-of like a global manner, available to everything contained in that component.

Context realizes the fact that only the components concerned with a given piece of data should know of it, NOT any intermediary components in between.

Without a doubt, context is an extremely useful feature at our disposal in React. However, it's important to note that it's not desirable to use context all the time without giving the least concern to whether we even really need it or not. We'll get to this later in this chapter when we learn when to and when not to use contexts.

Anyways, let's now see how to get started with a context in React.

But before that, here's a quick presentation of the application following from the example above, where we'll ultimately utilize context to pass some user data from App down to UserInfo.

For the purpose of demonstration, we don't need to worry about the definitions of Main, Nav, and Footer in the app; consequently, for the sake of brevity, we've omitted them from our discussion.

Following is the definition of App:

App.jsx
import { useState } from 'react';

function App() {
   const [user, setUser] = useState({
      name: 'Alice',
      desc: 'React developer'
   });

   return (
      <>
         <Sidebar user={user}/>
         <Main/>
      </>
   );
}

Here's that of Sidebar, inside the same App.jsx file containing the definition of App:

App.jsx
function Sidebar({ user }) {
   return (
      <div class="sidebar">
         <Header user={user}/>
         <Nav/>
         <Footer/>
      </div>
   );
}

Note that we're using this un-conventional approach of defining completely different components in the same App.jsx file only for the time being, to keep the demonstration of using context simple. Later on, in this chapter, we'll restructure this entire app based on the convention.

Anyways, following is the definition of Header:

App.jsx
function Header({ user }) {
   return (
      <header>
         <UserInfo user={user} />
      </header>
   );
}

And finally, here's the definition of UserInfo:

App.jsx
function UserInfo({ user }) {
   return (
      <p>{user.name} ({user.desc})</p>
   );
}

See how the user prop is defined in every component above. Currently, since our app uses prop drilling to pass on information, we need this prop, but later on, we'll let go off it in favor of a user context.

Below is a live example of what the app renders. (We've applied a little bit of styling to make the app seem bit more realistic):

Live Example

Great! With the stage all set, it's time to start exploring contexts in React.

How to create a context?

To create a context in React, we require the createContext() function exported by the react package.

As per its name,

createContext() creates a new context in React for passing given data down to deeper components without ... you know it ... using props.

More specifically, createContext() returns a context object that is used for setting up a component that allows its descendants to consume a given piece of data.

Syntactically, createContext() is very minimal:

createContext(defaultValue)

The optional defaultValue argument specifies some data to be used in case a consumer of the context isn't contained within a provider of the context. (More on providers and consumers shortly below.)

Let's start by creating a context to pass on some hypothetical user state data from an App component down to a descendant component. We'll call the context UserContext.

import { createContext, useState } from 'react';

const UserContext = createContext();
function App() { ... }

Notice how the context creation happens outside the App component. This is desirable because we need a reference to the context object returned by createContext() later on when needing the data stored in it.

With a context in hand, step two is to create its provider.

Creating the context's provider

Now, when working with context, there are two terms to take note of: a provider and a consumer.

A component that essentially provides the data to be used deeper down within bottom-level components via a context is called the context's provider.

On the same lines,

A component that reads that data — or better to say, consumes that data — is called the context's consumer.

After creating a context, the next logical step is to create a context provider.

After all, before being able to consume data, we have to provide it somewhere, right? In the case of contexts, this provision of data is done by creating a context provider.

The idea is simple: we wrap the entire component tree, where we ought to be able to pass data down without employing props, with a context provider component.

But where do we get this provider component? Well, this is the job of the Provider property of the object returned by createContext().

A property of a context object, Provider holds a component (i.e. a component function) that represents the underlying context's provider.

Quite simple, isn't it?

For class components, there is a second property of a context object worth noting: Consumer. This property points to a component meant for consuming data from the underlying context. Since we're using functional components, we don't need to be concerned with Consumer.

In the following code, we create a provider component for UserContext and render it as a child of the App component, since it's App within which we want the user state data to be available to every descendant component.

import { createContext, useState } from 'react';

const UserContext = createContext();

function App() {
   const [user, setUser] = useState({
      name: 'Alice',
      desc: 'React developer'
   });

   return (
      <UserContext.Provider>
         <Sidebar user={user}/>
         <Main/>
      </UserContext.Provider>
   );
}

So far so good.

After creating the provider component and rendering it in the appropriate place, we ought to pass on the desired data to it. This requires us to use the value prop of the provider component.

In the following code, since we need to provide the user state to all the elements inside App, we pass this user state to the newly-created context provider in its value prop:

import { createContext, useState } from 'react';

const UserContext = createContext();

function App() {
   const [user, setUser] = useState({
      name: 'Alice',
      desc: 'React developer'
   });

   return (
      <UserContext.Provider value={user}>
         <Sidebar user={user}/>
         <Main/>
      </UserContext.Provider>
   );
}

It's worthwhile noting here that a context can only take in one value to be passed down to its consumers.

We can't go around and add in an arbitrary number of props to a context provider hoping that they all get magically passed down to its consumers. No! Only a single value prop.

That's it.

But obviously because this value can be an object literal as well, we can place any number of properties in it and put all the necessary state data individually within these properties, being rest assured that its consumers will be able to easily take from the object whatever they seek to.

So now that the context has been created and its provider set up too, the only thing left is to consume the provided user data. And that requires us to know about the useContext() hook.

However, before we get into that, let's spare a few minutes and remove the user prop from every one of our components currently using it. This is because we've begun to transition to context and, therefore, don't need the props any longer:

import { useState } from 'react';

function App() {
   const [user, setUser] = useState({
      name: 'Alice',
      desc: 'React developer'
   });

   return (
      <>
         <Sidebar/>
         <Main/>
      </>
   );
}
function Sidebar() {
   return (
      <div class="sidebar">
         <Header/>
         <Nav/>
         <Footer/>
      </div>
   );
}
function Header() {
   return (
      <header>
         <UserInfo/>
      </header>
   );
}
function UserInfo() {
   return (
      <p>{user.name} ({user.desc})</p>
   );
}

Of course, at this point if we run the app, we'll get an error since user isn't available inside UserInfo.

As accomplished in the next section, this user data will instead come from consuming data from the created context.

The useContext() hook

The useContext() hook allows us to consume the data from a particular context.

Its syntax is shown as follows:

useContext(context)

context is a context object to consume data from.

Since it's a hook, our usage of useContext() has to follow all the rules of hooks in React.

In our case, the component that needs the user data defined inside App is UserInfo. Likewise, UserInfo would become a consumer of our UserContext context.

Let's do this now:

function UserInfo() {
   const user = useContext(UserContext);
   return (
      <p>{user.name} ({user.desc})</p>
   );
}

useContext() is called with UserContext (this context object is available inside UserInfo because the UserInfo() function is in the same App.jsx file where our context is defined).

useContext() returns back the given context's value, that is, the value provided to the context's provider's value prop. In our case, this value is the user state.

In the code above, we store this return value in user simply because the given data represents a user.

But because we're in UserInfo, where all we need to work with is user data and nothing else, we can even destructure the returned object into its constituent properties, name and desc. Something as follows:

function UserInfo() {
   const {name, desc} = useContext(UserContext);
   return (
      <p>{name} ({desc})</p>
   );
}

And that's how easy it is to enjoy the beauty of contexts in React.

Time to see this new rewrite of the app in action:

Live Example

Voila! Working just as before.

Let's recap all the steps that we accomplished thus far in order to set up a context in our app and consume its data:

  • Created a context using createContext(), in the top-level of the script, outside component code.
  • Created a provider of the context using the Provider property of the returned context object
  • Rendered this provider component right where we wanted to be able to set up a context for passing data down, i.e. inside App.
  • Coded the subtree as normal, i.e. nested <Sidebar> and <Main> inside the provider component just as it was nested before inside App.
  • Called useContext() in the component where the context's data was required, i.e. in UserInfo. The returned value represented the user data.

And that's it!

So what do you make of a React context? Easy or difficult?

There's more description to the return value of useContext() when a component using this hook is NOT contained within a provider of the given context, but for now, let's keep things simple and leave such complexities for later on.

Conventional way of working with contexts

In the example app above, even though the usage of contexts is absolutely perfect, the convention is not to create contexts in the same file where its provider is created within a component.

"Why?" you ask. Well, it ain't difficult to envision this.

Conventionally, as we even did in the previous chapters, React apps are built with each component allotted a separate file.

Let's say our demo application above was built in the same way. Based on this, if we create a context in App.jsx, render its provider inside the App component, and then want to use the context in another component in a file UserInfo.jsx, what would we do?

Well, you might say that we could export the context object as a named export from App.jsx and then use it in UserInfo.jsx by importing it from App.jsx. Technically, yes that's completely doable.

But it won't take any longer than ten seconds for us to realize that this is an example of bad program design. Why import App.jsx in UserInfo.jsx? Why is the main app file a dependency of UserInfo.jsx?

Doesn't seem sensible, right?

The conventional approach in React when working with contexts is to create a separate file for every context.

As you can probably foresee, one apparent export of such a file is a context object. There can be other exports as well, but at least the context object is always one.

Let's rewrite the example app above using this approach.

First off, we'll create a separate file called UserContext.js and shift the UserContext definition from App.jsx into it and also make it a named export:

UserContext.js
import { createContext } from 'react';

export const UserContext = createContext();

If you're wondering why we make the context object a named export and not a default export, well that's because there are other things to be exported from a context file besides the context object as discussed next.

Now, we need to export the provider of the context:

UserContext.js
import { createContext } from 'react';

export const UserContext = createContext();
export const UserProvider = UserContext.Provider;

Although, precisely this export isn't required, we do it in order to have all the context setup logic nicely within the file where it's created.

After this change, here's how App.jsx looks:

App.jsx
import { useState } from 'react';
import { UserProvider } from './UserContext';

function App() {
   const [user, setUser] = useState({
      name: 'Alice',
      desc: 'React developer'
   });

   return (
      <UserProvider value={user}>
         <Sidebar/>
         <Main/>
      </UserProvider>
   );
}

All the context setup logic has gone away, thanks to encapsulating all of it within UserContext.jsx.

Let's now refactor every single component of the app into its own, individual file:

Sidebar.jsx
function Sidebar() {
   return (
      <div class="sidebar">
         <Header/>
         <Nav/>
         <Footer/>
      </div>
   );
}
Header.jsx
function Header() {
   return (
      <header>
         <UserInfo/>
      </header>
   );
}
UserInfo.jsx
import { useContext } from 'react';

function UserInfo() {
   const user = useContext(UserContext);
   return (
      <p>{user.name} ({user.desc})</p>
   );
}

The final step now is to import the context object exported by UserContext.js in UserInfo.jsx and ultimately pass it on to useContext() inside the UserInfo component:

This is done as follows:

UserInfo.jsx
import { useContext } from 'react';
import { UserContext } from './UserContext';

function UserInfo() {
   const user = useContext(UserContext);
   return (
      <p>{user.name} ({user.desc})</p>
   );
}

And this completes our app's rewrite.

Live Example

The output is the same as before, just this time we've followed the conventional way of working with context in React — created a separate file for the context logic and then name-exported all the required stuff from it.

Preventing re-rendering everything

One immediate concern of using context amongst sound React developers is about the potential problem of re-rendering everything once a context's value changes.

That is, if context resides right near the top-level App component, what would happen if some state data changes in App that is only concerned with some very deep components using the state value?

Should the entire app re-render?

Let's understand this concern with the help of an example.

Suppose each of the components of our demo app, when rendered, logs its name along with the text 'rendering'. So for instance, when Header renders, it logs 'Header rendering'.

With this in mind, consider the following definition of UserInfo:

UserInfo.jsx
import { useContext } from 'react';
import { UserContext } from './UserContext';

function UserInfo() {
   const {user, setUser} = useContext(UserContext);
   return (
      <>
         <p>{user.name} ({user.desc})</p>
<button onClick={() => setUser({ name: 'Charlie', desc: 'Perl developer' })}> Change user to Charlie </button>
</> ); }

We've made one noticeable change here: a button serves to change the user from Alice to Charlie.

This change happens by leveraging the setUser() state-updater function as defined inside App. How we're able to retrieve this function is by providing it to UserProvider upon instantiation, as shown below:

App.jsx
function App() {
   const [user, setUser] = useState({
      name: 'Alice',
      desc: 'React developer'
   });

   return (
      <UserProvider value={{ user, setUser }}>
         <Sidebar/>
         <Main/>
      </UserProvider>
   );
}

Previously, in App, we directly provided user to the value prop of UserProvider, however this time we provide an object literal containing two properties: user and setUser.

This also explains why we employ the destructuring syntax when defining the user and setUser constants inside UserInfo above.

Let's click the button now and see what gets rendered.

Live Example

Here's the console snippet after clicking on the button:

App rendering Sidebar rendering Header rendering UserInfo rendering Nav rendering Footer rendering Main rendering

As you can see here, when the user state changes in App, everything inside App, including itself, re-renders.

This re-rendering of everything is precisely the problem.

In real-world apps, the level of nesting is magnitudes larger than this, and in that scenario, re-rendering the entire application starting from the point where the state data changes is undesirable.

So how can we mitigate this?

How can we make sure that when some state changes, whereby it's passed on to a context provider, the context provider does NOT re-render its entire subtree again?

Well, that requires a beautiful pattern in React. Let's see what it is.

Notice the App component's definition above:

App.jsx
function App() {
   const [user, setUser] = useState({
      name: 'Alice',
      desc: 'React developer'
   });

   return (
      <UserProvider value={{ user, setUser }}>
         <Sidebar/>
         <Main/>
      </UserProvider>
   );
}

We have the user's state data being managed inside App.

Can't this be improved?

If we think for a while, the user data is better off at being managed in the UserContext.js file instead of being managed in App.jsx because that's where we have all the user context–related utilities.

But before we can manage the user state in a different component, we need one such component.

One option is to create a separate component, defined in the context file, containing everything contained in App (including the state logic), and then have this component rendered in App.

Something like this:

App.jsx
import { UserData } from './UserContext';

function App() {
   return (
      <UserData/>
   );
}
UserContext.js
import { createContext } from 'react';

export const UserContext = createContext();

export function UserData() { const [user, setUser] = useState({ name: 'Alice', desc: 'React developer' }); return ( <UserContext.Provider value={{ user, setUser }}> <Sidebar/> <Main/> </UserContext.Provider> ); }

However, the problem with this is that all the other components of the app that don't necessarily concern themselves with the user context are being instantiated in UserContext.js.

UserContext.js must only contain stuff related to user data, NOT the <Sidebar> and <Main> or any other elements. The constituent components of App must be mentioned within App itself.

So this option becomes, more or less, useless.

The second option, which is the recommended one, is to have a separate component as before but this time with all of its children obtained from App.

Something like this:

App.jsx
import { UserData } from './UserContext';

function App() {
   return (
      <UserData>
<Sidebar/> <Main/>
</UserData> ); }

Now, since UserData doesn't itself define all of its children but rather gets them from the parent component, App, it must render its children prop.

Here's the new definition of UserData:

UserContext.js
import { createContext } from 'react';

export const UserContext = createContext();

export function UserData({ children }) {
   const [user, setUser] = useState({
      name: 'Alice',
      desc: 'React developer'
   });

   return (
      <UserContext.Provider value={{ user, setUser }}>
         {children}
      </UserContext.Provider>
   );
}

Perfect.

If we closely notice, because the component UserData is being used as a context provider, we should better update its name from UserData to UserProvider:

UserContext.js
import { createContext } from 'react';

export const UserContext = createContext();

export function UserProvider({ children }) {
   const [user, setUser] = useState({
      name: 'Alice',
      desc: 'React developer'
   });

   return (
      <UserContext.Provider value={{ user, setUser }}>
         {children}
      </UserContext.Provider>
   );
}

Following this change, we ought to update UserData inside App.jsx:

App.jsx
import { UserProvider } from './UserContext';

function App() {
   return (
      <UserProvider>
         <Sidebar/>
         <Main/>
      </UserProvider>
   );
}

Here's the app we get with all these changes:

Live Example

Go on and click the button again and see what gets logged this time.

So what did you see?

Well, the moment we press the button to update the user state (currently being managed inside the UserProvider component), here's what gets logged:

UserProvider rendering UserInfo rendering

Only UserProvider is rendered and then the bottom-level component, UserInfo. Nothing — we repeat, nothing — in between these two components is re-rendered.

And that is phenomenal!

The reason why this happens here is because of the children prop that we added to UserProvider.

The thing is that now the state data is being managed inside UserProvider while the entire subtree of components is defined inside App.

The effect of this is that when the user state changes of UserProvider, it re-renders as before, but this time its children prop remains the same and thus the rendered UserContext.Provider component gets rendered with the same children prop.

Even if React were to process this children prop (which it doesn't in order to conserve further computation resources), it won't be calling the corresponding component for every element therein since the components have already been rendered in the past to ultimately give this children prop.

In other words, with the children prop, React already has a list of the children to render in UserProvider; it does NOT have to reinvoke their corresponding components for re-forming the list.

This has the wonderful effect of preventing everything contained inside a context's provider from re-rendering. Only the components that consume the context are re-rendered.

Now an interesting question to ask at this stage is: How does React know which components to re-render when a particular context's value changes?

Hmm. Indeed an interesting question.

What do you think? How does React know this? Let's see...

(opt) The internals of contexts

When useContext() is called within a component in React, a reminder is logged somewhere in the context that the underlying component instance is tied to it.

This isn't difficult to do for React. When it invokes a component for rendering, it knows that it is currently inside that component. Thereafter, when it encounters the useContext() hook along with the context object in that component, it has everything it needs to store the component in some way inside the context object.

Now on a subsequent re-render of the context's provider, React would know exactly which components down the stream rely on the context's data and would then go on and only render those components.

Why does React do this? Why does React store the set of components that rely on a context in that context?

Well, as you can guess, it's highly desirable to do so.

No one would want to render the entire application again upon the change of a given context's data; React recognizes this, as would any performant library, and stores the set of components dependent upon a context so that it only renders them when the context's data changes.

But of course, as we saw above, this fact only applies when we render the children prop within the context provider component, thus letting it skip re-rendering its entire subtree.

If we render the subtree components directly within the provider, its children prop would be different on each one of its re-renders and thus result in the entire subtree of components to re-render all over again, as we saw in the preceding discussion.

Besides this, there is a lot more to the internals of contexts in React. A great resource to learn more about them is this one by JSer.dev: How does Context work internally in React?.

Default context value

When useContext() is called in a component, React has to resolve the value that it returns. The algorithm for determining this value is quite straightforward.

To begin with, React confirms whether the component where useContext() is called with a context object is a descendant of that context's provider or not.

This is done by traversing up the component tree and checking whether the current parent is the context's provider component.

  • If the component is a descendant of the context's provider (that is passed on to useContext()), the useContext() call is resolved with that provider's value, i.e. with its value prop.
  • However, if this isn't the case, the useContext() call is resolved with the default value of the underlying context, i.e. the one provided at the time of creating the context via createContext().

Shown below is a very simple example illustrating this:

App.jsx
function App() {
   return (
      <>
         <ExtraInfo/>
         <UserProvider>
            <Sidebar/>
            <Main/>
         </UserProvider>
      </>
   );
}

We are adding a sibling component, ExtraInfo, prior to UserProvider for the sake of testing the effect of calling useContext() in a component not contained within a context.

Most importantly, notice how <ExtraInfo> is outside the <UserProvider> element; in effect, it's outside of our context provider.

Following we have the definition of ExtraInfo, obviously calling on to useContext() because that's what we're here to test, right?

ExtraInfo.jsx
import { useContext } from 'react';
import { UserContext } from './UserContext';

function ExtraInfo() {
   const { user } = useContext(UserContext);

   return (
      <p>ExtraInfo: {user.name} ({user.desc})</p>
   );
}

Before we run our app again, let's go on and change the definition of UserContext, by adding a default value while invoking createContext():

UserContext.js
import { createContext } from 'react';

export const UserContext = createContext({
user: { name: 'Default user', desc: 'Default developer' }
}); ...

Alright, let's now run the app and see what is rendered inside the <ExtraInfo> element:

Live Example

As can be seen, we get the text 'Alice' inside the <UserInfo> element but the text 'Default user' inside <ExtraInfo>.

And you know why that's the case, right?