React Context
A new way to share data between components in your React application
prerequisites
- basic JS syntax
- React components, including props and state
- JSX
tldr;
- In addition to props, Context is now the only other way to share data in a React component tree
- It allows deeply nested components to access shared data, without passing that data down every level via props
What's a Context?
Context is a construct built into the React framework, used to share data between components. It has a reputation for being difficult to understand. Maybe this is because it's often found in large and complex applications. However, the way Context works is actually very simple.
At it's core, Context is just a way to share data between components in a React application.
Context provides a way to pass data through the component tree without having to pass props down manually at every level.
React documentation
Before Context, the only way to share data between components was to pass that data directly between parents and children, using props.
Context aims to share that same data between those same components, but without needing to pass it directly via props. Aside from props, it is the only other built-in way to share data between components in a React component tree.
How does it work?
Since props and Context are both are solutions to the same problem, let's compare the mechanics of both, to see how they solve the problem differently.
Props allow data to be passed down a component tree. Parent components hand off data directly to their child components, during render. Context similarly allows data to be passed down a component tree. The difference is that Context allows a component to share data with any component existing below it in the component tree.
While props allow a direct hand-off of data between parent and child, Context instead allows a parent component to put aside a specific set of data, without directly passing that data to a child. This data is held outside the component tree, and any child component existing beneath that parent component has the ability to reach out and access it.
You can see that there is no direct hand-off of data between the parent and child component when using Context. The parent component simply says "hey, here's some data I would like to share". The child component could access that data, or it could not. The data offered up by the parent component is held in a kind of data store, and all of the components existing below that parent component have the ability to access it, if they want to.
In a component tree with just one parent and one child component, it doesn’t make much difference if we pass data down via props or Context. But what if we had a larger component tree, where we need to share data many levels down?
This is the key to understanding what Context can offer, in contrast to props. It allows data in a parent component to be shared with components below it in a component tree, without needing to hand down that data at every level via props. This opens new doors for how we think about application architecture. It's a new tool in our toolbox for building React applications.
The API
So how does this actually work in our code?
If we think generally about other ways that data gets shared and passed around in code - what are the main principles that enable two different pieces of code to successfully share data between them? These two different pieces of code need some kind of contract. Something to enforce that the data one piece of code is sharing will match the kind of data the other piece of code is expecting.
In a plain old function, this contract is the function arguments. The function definition declares what kind of data it's expecting as arguments. Then the code that calls the function will share data from its own scope by passing arguments in the expected form as the function is called.
// this function defines the data it's expecting to be passed
// as function arguments (in this case, a single number)
function logNumber(number) {
console.log("Number is " + number % 2 ? "odd." : "even.");
}
const inputNumber = getUserInput();
// when the logNumber function is called, the code calling that
// function passes data from its own scope as arguments,
// which match the form declared in function definition
logNumber(inputNumber);
What about sharing data in a React application?
In React components, data is always shared top-down, from parent components to child components. The contract that defines what data can be shared between parent and child components is the list of props. The child component declares what kind of data it’s expecting as props, and the parent component shares data from its own scope by passing prop values to that child component as it is rendered.
// the child component defines the data it's expecting to be passed
// as props (in this case, two strings)
function ChildComponent({firstName, lastName}) {
return (
<div>
Welcome, {firstName} {lastName}!
</div>
);
}
function ParentComponent() {
const userFirstName = "Amelia";
const userLastName = "Earhart";
// when the ChildComponent is rendered, the parent
// component passes data from its own scope as prop values,
// which match the props declared in ChildComponent definition
return (
<div>
Hello world.
<ChildComponent
firstName={userFirstName}
lastName={userLastName}
/>
</div>
);
}
Just like sharing data via function arguments or props, Context must enable some kind of contract between the component sharing data, and the component receiving data. Something must enforce that the data being shared from the parent component will match the kind of data the child component is expecting.
This is called a Context object.
const MyNewContext = React.createContext(defaultValue);
You can think of a Context object as defining the shape of data to be shared between parent and child components (indeed if you’re using TypeScript this is exactly the case).
The only thing needed to create a new Context object is a default value, matching that shape of data. The default value is a safegaurd, just in case there is a child component reading from this Context object, but no parent component above it providing a value. (Don't worry too much about this - in practice you would almost always want there to be at least one parent component providing data and child component consuming it)
const MyNewStringContext = React.createContext("");
const MyNewNumberContext = React.createContext(0);
const MyNewApplicationContext = React.createContext({
counterValue: 0,
addCount: () => {}
})
The default values in a Context object tell us what the shape of any data being shared via that Context object should be. So now, how do we actually use it? How do we share data between parent and child components via a Context object?
The Context API provides two important ways to interact with a Context object. One is a way to provide data, from the scope of the parent component, to a Context object. This is done via a property called "Provider", attached to the Context object. This Provider is actually a react component, which you can use in your component tree. It expects just one prop - "value". The value prop accepts the data that will be shared with the child components. This data should match the expected shape, as defined when the Context object was created.
<MyNewStringContext.Provider value={"string from parent component"}>
<ChildComponents />
</MyNewStringContext.Provider>
The other way to interact with a Context object is by reading the current value. This can be done several different ways - we will go over the most recent form here (using hooks). To learn about the other two methods (using a "Consumer" component, or "contextType" on a class component), see the official documentation.
const currentContextValue = React.useContext(MyNewStringContext);
When calling the useContext hook inside a child component, it will look up the component tree until it finds a parent that has provided a value for that Context object. It will then return the current value provided by that parent component.
If the Context object is a contract defining how data will be shared between components, you can think of the Provider and useContext mechanisms as ways to interact with individual instances of that contract.
Let's quickly recap. Here are the three steps to successfully share data between components via Context:
- create a new Context object, defining what kind data can be shared
- in the parent component which contains the data to be shared, use the Provider component to make that data available for consumption by components lower in the tree
- in child components wanting to access the data being shared, use the useContext hook (or one of the other two methods) to access the data currently being shared by a parent component, for that Context object
Here's what it looks like all together.
const MyNewStringContext = React.createContext("");
function ParentComponent() {
const stringToShare = "Hello, world!";
return (
<MyNewStringContext.Provider value={stringToShare}>
<div>
Welcome.
<ChildComponent />
</div>
</MyNewStringContext.Provider>
)
}
function ChildComponent() {
const stringFromParent = React.useContext(MyNewStringContext);
return (
<span>This is the message: {stringFromParent}</span>
)
}
Now if the value of stringToShare changes, any child component reading from the MyNewStringContext will automatically get the updated value. React subscribes the child component to any changes in the Context value, similar to how child components subscribe to any changes in the prop values passed by a parent component. Anytime that value changes, the child component will get the updated value, and will re-render.
It's important to note that the same Context object can be used many times. It's simply a definition, which can be implemented by different providers.
const MyNewStringContext = React.createContext("");
function ParentComponent() {
const string1 = "string one";
const string2 = "string two";
return (
<div>
<MyNewStringContext.Provider value={string1}>
<ChildComponent />
</MyNewStringContext.Provider>
<MyNewStringContext.Provider value={string2}>
<ChildComponent />
</MyNewStringContext.Provider>
</div>
)
}
In the same vein, if a child component reads from a Context object, it is possible that multiple parent components have provided values for that Context object. In this case, the child component will read from the provider living in the parent closest to itself in the component tree.
const MyNewStringContext = React.createContext("");
function ParentComponent() {
const string1 = "string one";
return (
<MyNewStringContext.Provider value={string1}>
<IntermediateParentComponent />
</MyNewStringContext.Provider>
);
}
function IntermediateParentComponent() {
const string2 = "string two";
return (
<MyNewStringContext.Provider value={string2}>
<ChildComponent />
</MyNewStringContext.Provider>
);
}
function ChildComponent() {
// the value of stringValue will be "string two", since
// IntermediateParentComponent is closer in the component tree
const stringValue = useContext(MyNewStringContext);
return <div>{stringValue}</div>;
}
Where do we go from here
Ok, great. Now we know how Context works. Pretty simple, right?
Notice how we've made no mention so far of state. If you have heard of Context before, you've probably heard how it's here to replace Redux, or that it can help scale your application. But really there is nothing about Context directly related to state, or your application design. At its most basic, Context is just a mechanism to share data between React components. That's it.
Context is a new tool in our toolbox. It's a new way of thinking about how data moves through our React applications. This means it is now up to us to explore how Context can really be used in our applications. As with any new tool, we need to think critically about all the different ways it can be used. What new doors does it open for how we can architect our applications? What are the ways it can be mis-used? What are the implications when using Context instead of props?
These are hard questions to answer. But if they sound interesting to you, join us in the next chapter - React Context, In the Real World.