6.2 - Passing Data Deeply with Context
Usually, you will pass information from a parent component to a child component via props. But passing props can become verbose and inconvenient if you have to pass them through many many components.
Wouldn’t it be great if there were a way to “teleport” data to the components in the tree that need it without passing props?
Context: an alternative to passing props
Context lets a parent component provide data to the entire tree below it, instead of using props. Consider this Heading component that accepts a level
for its size:
- App.js
- Section.js
- Heading.js
import Heading from './Heading.js';
import Section from './Section.js';
export default function Page() {
return (
<Section>
<Heading level={1}>Title</Heading>
<Heading level={2}>Heading</Heading>
<Heading level={3}>Sub-heading</Heading>
<Heading level={4}>Sub-sub-heading</Heading>
<Heading level={5}>Sub-sub-sub-heading</Heading>
<Heading level={6}>Sub-sub-sub-sub-heading</Heading>
</Section>
);
}
export default function Section({ children }) {
return (
<section className="section">
{children}
</section>
);
}
export default function Heading({ level, children }) {
switch (level) {
case 1:
return <h1>{children}</h1>;
case 2:
return <h2>{children}</h2>;
case 3:
return <h3>{children}</h3>;
case 4:
return <h4>{children}</h4>;
case 5:
return <h5>{children}</h5>;
case 6:
return <h6>{children}</h6>;
default:
throw Error('Unknown level: ' + level);
}
}
Let’s say you want multiple headings within the same Section
to always have the same size. If you were to do this with props, you might pass a level
prop to each <Heading>
separately:
<Section>
<Heading level={3}>About</Heading>
<Heading level={3}>Photos</Heading>
<Heading level={3}>Videos</Heading>
</Section>
It would be nice if you could pass the level prop to the <Section>
component instead and remove it from the <Heading>
. This way you could enforce that all headings in the same section have the same size:
<Section level={3}>
<Heading>About</Heading>
<Heading>Photos</Heading>
<Heading>Videos</Heading>
</Section>
But how can the <Heading>
component know the level of its closest <Section>
? You can'd do it with props alone. This is where context comes into play.
Context lets the parent component make some information available to any component in the tree below it—no matter how deep—without passing it explicitly through props.
For this example, you will have Section
provide a context. (You can call it LevelContext
, since it’s for the heading level.) Heading
will read from LevelContext
to know what level
value to use.
Step 1: Create the context
First, you need to create the context. You’ll need to export it from a file so that your components can use it:
- LevelContext.js
import { createContext } from 'react';
export const LevelContext = createContext(1);
The only argument to createContext
is the default value. We've set to 1. (Here, 1 refers to the biggest heading level).
Step 2: Use the context
In your file containing the Heading
component, import the useContext
Hook from React and your context:
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
Change the Heading
component to have it read the value from the context you just created:
export default function Heading({ children }) {
const level = useContext(LevelContext);
// ...
}
useContext
is a Hook. Just like useState
and useReducer
, you can only call a Hook immediately inside a React component (not inside loops or conditions). useContext
tells React that the Heading
component wants to read the LevelContext
.
Now that the Heading
component doesn’t have a level
prop, you don’t need to pass the level
prop to Heading
in your JSX like this anymore:
<Section>
<Heading level={4}>Sub-sub-heading</Heading>
<Heading level={4}>Sub-sub-heading</Heading>
<Heading level={4}>Sub-sub-heading</Heading>
</Section>
Update the JSX so that it’s the Section
that receives it instead:
<Section level={4}>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
</Section>
Step 3: Provide the context
So far, the Heading
components will all use the default level
of 1 we set previously. We now need to tell React that each Section
component provides context to its children Heading
components.
The Section
component currently renders its children:
export default function Section({ children }) {
return (
<section className="section">
{children}
</section>
);
}
Wrap them with a context provider to provide the LevelContext
to them:
import { LevelContext } from './LevelContext.js';
export default function Section({ level, children }) {
return (
<section className="section">
<LevelContext.Provider value={level}>
{children}
</LevelContext.Provider>
</section>
);
}
This tells React: “if any component inside this <Section>
asks for LevelContext
, give them this level.” The component will use the value of the nearest <LevelContext.Provider>
in the UI tree above it.
Using and providing context from the same component
Since context lets you read information from a component above, each Section
could read the level from the Section
above, and pass level
+ 1 down automatically. Here is how you could do it:
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
export default function Section({ children }) {
const level = useContext(LevelContext);
return (
<section className="section">
<LevelContext.Provider value={level + 1}>
{children}
</LevelContext.Provider>
</section>
);
}
With this change, you don’t need to pass the level prop either to the <Section>
or to the <Heading>
:
- App.js
- Section.js
- Heading.js
- LevelContext.js
import Heading from './Heading.js';
import Section from './Section.js';
export default function Page() {
return (
<Section>
<Heading>Title</Heading>
<Section>
<Heading>Heading</Heading>
<Heading>Heading</Heading>
<Heading>Heading</Heading>
<Section>
<Heading>Sub-heading</Heading>
<Heading>Sub-heading</Heading>
<Heading>Sub-heading</Heading>
<Section>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
</Section>
</Section>
</Section>
</Section>
);
}
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
export default function Section({ children }) {
const level = useContext(LevelContext);
return (
<section className="section">
<LevelContext.Provider value={level + 1}>
{children}
</LevelContext.Provider>
</section>
);
}
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
export default function Heading({ children }) {
const level = useContext(LevelContext);
switch (level) {
case 0:
throw Error('Heading must be inside a Section!');
case 1:
return <h1>{children}</h1>;
case 2:
return <h2>{children}</h2>;
case 3:
return <h3>{children}</h3>;
case 4:
return <h4>{children}</h4>;
case 5:
return <h5>{children}</h5>;
case 6:
return <h6>{children}</h6>;
default:
throw Error('Unknown level: ' + level);
}
}
import { createContext } from 'react';
export const LevelContext = createContext(0);
It’s not always better to use context instead of props! Sometimes props can help with readability if your data flow is very complex by making it explicit which component passed information to which component.