Author avatar

Arnaud Christ

How to Use React Context to Share Data between Components

Arnaud Christ

  • Jun 29, 2020
  • 9 Min read
  • 2,501 Views
  • Jun 29, 2020
  • 9 Min read
  • 2,501 Views
Web Development
Front End Web Development
Client-side Framework
React

Introduction

There are different ways to share data between React components. The first one, which you may already know about if you are a React developer, is to use props to pass data down from one component to another. But when things start getting a bit serious, you know that this approach has its own limits. You may have tried using a third-party library to handle your app state with libraries like Redux. Libraries to manage the state of your app like Redux are great, but depending on your use case, they might be a bit heavy to use. This guide will focus on a built-in solution in the React library called React Context.

Sharing Data Between Components

Spreading Props to Child Components

This is React 101, showing that you can pass down data from one component to an other using props.

1
2
3
4
const Application = () => {
  const title = "Foobar";
  return <Section title={title} />;
};
javascript

The example above describes the implementation of a component called Application. The only thing it does is renders Section and passes it a title prop set to ”Foobar”

Then in the implementation of Section you can access this prop like this:

1
2
3
const Section = ({ title }) => {
  return <span>{title}</span>;
};
javascript

If you are not using a strongly typed language, like TypeScript for instance, but still want some type checking on your props, the React team has published a third-party library called prop-types

In just a few additional lines of code, you could indicate that the title prop of your Section component is a string and is required. Here is what your Section.js file could look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from "react";
import PropTypes from "prop-types";

const propTypes = {
  title: PropTypes.string.isRequired
};

const Section = ({ title }) => {
  return <span>{title}</span>;
};

Section.propTypes = propTypes;

export default Section;
javascript

While this solution looks nice in the above example, it starts to be cumbersome when you have to pass data from one component to another multiple levels below in the tree of components. You then end up having components with props they are not really using themselves but just passing down to other components.

React Context

To share data between your components you used to basically have to choose between using props and using a third-party library to manage the state of your app. The React team worked on a built-in solution and introduced React Context in React 16.3.0.

Sharing Data Using React Context

React Context allows you to pass data through a component tree without having to explicitly pass them through props or using any third-party library. This guide will only focus on how to use React Context in functional components as it is now the de facto way of creating React components. Please note that React Context can also be used in class-based components.

How Does It Work?

To illustrate how React Context could help you manage data in your app, this guide will go through a quick real-life use case that you may have all already been through in your professional life. Let's say you are asked to develop an app responsible for managing unicorn races (you've been there before, right?).

In this app, you have a list of all the past races with a filter on the type of unicorns because, you know, unicorns may or may not have wings, a horn, or none of those at all (in which case, they are called horses). You also have somewhere else, a form to create unicorn races where one of the fields is the type of unicorns allowed in the race.

As this list of unicorn types is administrated by the Global Federation Of Unicorns, it is exposed by an external API.

To keep it simple, you could fetch this API every time you need to use this list of types. But this means you will trigger multiple times the same HTTP request to fetch the same data. What you could do instead, is to fetch this reference data when you start your app and then store it somewhere in order to be reused later.

That's where React Context could be interesting.

Start by creating a ReferenceDataContext.js file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { createContext, useState } from "react";

const [unicornTypes, setUnicornTypes] = useState(undefined);

ReferenceDataContext = createContext({ unicornTypes, setUnicornTypes });

const ReferenceDataContextProvider = ({ children }) => {
  return (
    <ReferenceDataContext.Provider value={{ unicornTypes, setUnicornTypes }}>
      {...children}
    </ReferenceDataContext.Provider>
  );
};

export { ReferenceDataContext, ReferenceDataContextProvider };
javascript

To create a Context, React exposes an API called createContext. This function takes the default state of your Context and returns a Context object. Every Context object comes with a Provider React component allowing all its children components to subscribe to this Context.

Here created a small ReferenceDataContextProvider component was created, which is just wrapping its children with the Context Provider it just got from the call to createContext.

As this is reference data that you might have to access from anywhere in your app, you will have to wrap your whole app with this Provider. Let's say that you have an app component that is your top level component.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { useContext }, React from "react";
import { ReferenceDataContext, ReferenceDataContextProvider } from "./ReferenceDataContext"
import Main from " ./Main"

const App = () => {
  const { unicornTypes } = useContext(ReferenceDataContext)

    return (
        <ReferenceDataContextProvider>
          <Main />
        </ReferenceDataContextProvider>
    )
}

export default App
javascript

Here the app is wrapped at top level with this Provider because the data it exposes might be required anywhere in the app, but you can imagine having Providers only wrapping sub-trees of your app if needed. That is even actually a good idea, as this guide will explain a bit later.

In the Main component, fetch the types from the API and store them in your Context. To simplify the syntax a bit, assume that you have the library axios installed in your npm dependencies to deal with HTTP requests, but feel free to use whatever solution suits your needs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { useContext, useEffect, useState }, React from "react"
import { ReferenceDataContext } from "./ReferenceDataContext"
import axios from "axios"

const Main = () => {

  const [isLoading, setIsLoading] = useState(false);
  const { unicornTypes, setUnicornTypes } = useContext(ReferenceDataContext);

    useEffect(() => {
      setIsLoading(true)
      axios.get("PATH_TO_THE_API")
        .then(response => response.data)
        .then(setUnicornTypes)
        .finally(() => setIsLoading(false))
    }, [axios, setUnicornTypes])

    const renderTypesList = () => {
      unicornTypes.map(type => <li>{ type }</li>)
    }

    return (
      <>
        {isLoading && <p>Loading...</p>}
        { !isLoading && (
          <ul>
            {renderTypesList()}
          </ul>
        ) }
      </>
    )
}

export default Main
javascript

In this file, when the types are fetched from the API, you'll store them in the context. You are also using the types in the render part of your component when we list them. As the component subscribed to the context values as it imports some data from it, it gets automatically re-rendered when the list of types gets updated.

Caveats

React Context is a great API and, as mentioned before, it can be a good idea to multiply the Providers in your app instead of having only one global one to store all your data. You might be tempted to store all your data and wrap your whole app with a Provider providing all this data to your app. The main issue with that approach is that every component subscribing to the Context gets re-rendered every time the value passed to the Provider change. This means that you might end up having a lot of extra rendering when updating only one value in your Context.

A workaround for that is to have a dedicated Context/Provider for each functional section that share data between its own components. Then you will reduce the number of components rendering as you will only have updates on sub-trees of your app.

Conclusion

In this guide, we explored how we can easily use React Context instead of passing down props to share data between components. Depending on your use case, you might prefer to use simple props, React Context or even a third-party library like Redux to share data between your components. Those three solutions are perfectly valid and production-ready.

15