99

Concurrent visitors over the last 30 minutes. (~)

Peak concurrent visitors -
Yesterday: 0 / Today: 0

React Context API Essentials

React Context API Essentials

The examples in this post are based on the project created in Starting a React Project with Vite.

The React Context API is a way to share data between ancestor and descendant components in a project.
Let's start from the following project structure and learn the concepts and usage of the Context API.

1
2
3
4
5
6
7
8
9
├─src/ │ ├─components/ │ │ ├─A1.tsx │ │ ├─A2.tsx │ │ ├─B1.tsx │ │ ├─B2.tsx │ │ ├─B3_1.tsx │ │ └─B3_2.tsx │ └─App.tsx
Project structure

Component structure

# Props

Props are the main way a parent component passes data to a child component.
You can pass data very easily, but if you want to pass data further than a child component, the components in between have to handle data they do not need.

Let's look at the example below to understand this.
We want to use the reactive color data (red) managed by the App component in the A2 and B3_2 components.
Then the A1 component, which sits between App and A2, has to pass the data along, and likewise the B1 and B2 components, which sit between App and B3_2, have to pass the data along.

Passing data with props

This situation is called 'prop drilling' because the props (data) seem to drill through several components.

Prop drilling

/src/App.tsx
TSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { useState } from 'react' import A1 from './components/A1' import B1 from './components/B1' export default function App() { const [color] = useState('red') return ( <> <div>App</div> <A1 color={color} /> <B1 color={color} /> </> ) }
Passing data with props
/src/components/A1.tsx
TSX
1
2
3
4
5
6
7
8
9
10
import A2 from './A2' export default function A1({ color }: { color: string }) { return ( <> <div>A1</div> <A2 color={color} /> </> ) }
Handling data it does not need
/src/components/A2.tsx
TSX
1
2
3
export default function A2({ color }: { color: string }) { return <div style={{ color }}>A2</div> }
/src/components/B1.tsx
TSX
1
2
3
4
5
6
7
8
9
10
import B2 from './B2' export default function B1({ color }: { color: string }) { return ( <> <div>B1</div> <B2 color={color} /> </> ) }
Handling data it does not need
/src/components/B2.tsx
TSX
1
2
3
4
5
6
7
8
9
10
11
12
import B3_1 from './B3_1' import B3_2 from './B3_2' export default function B2({ color }: { color: string }) { return ( <> <div>B2</div> <B3_1 /> <B3_2 color={color} /> </> ) }
Handling data it does not need
/src/components/B3_1.tsx
TSX
1
2
3
export default function B3_1() { return <div>B3_1</div> }
/src/components/B3_2.tsx
TSX
1
2
3
export default function B3_2({ color }: { color: string }) { return <div style={{ color }}>B3_2</div> }

# Context API

To overcome the limits of the props approach we just looked at, React provides the Context API.
The Context API lets data provided by a specific component be used by its descendant components, including its children.

TSX
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
import { createContext, useContext } from 'react' // Create a context with an initial value const MyContext = createContext(data) // Provide data with the context provider <MyContext.Provider value={data}> {children} </MyContext.Provider> // Get data by using the context const data = useContext(MyContext) // Get data with the context consumer (optional) <MyContext.Consumer> {data => <div>{data}</div>} </MyContext.Consumer> // Name the context (optional) MyContext.displayName = 'MyContext'
Context API

Again, we want to use the color data of the App component in the A2 and B3_2 components.
To do this, we can use the Context API so that the App component provides the color data and its descendant components use it.

Providing and using data with the Context API

In the App component, call the createContext function to create a context with an initial value.
Wrap the descendant components that should receive the data with the .Provider component of the created context object, and provide the data to pass with the value prop.
Now the descendant components can use the data.

/src/App.tsx
TSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { createContext } from 'react' import A1 from './components/A1' import B1 from './components/B1' export const ColorContext = createContext('') export default function App() { const [color] = useState('red') // Color data return ( <ColorContext.Provider value={color}> <div>App</div> <A1 /> <B1 /> </ColorContext.Provider> ) }

Unlike the props approach, components that do not use the provided data have nothing extra to handle.

/src/components/A1.tsx
TSX
1
2
3
4
5
6
7
8
9
10
import A2 from './A2' export default function A1() { return ( <> <div>A1</div> <A2 /> </> ) }
Nothing to handle

In components that use the data, you can get the provided data with the useContext hook.
When you use the useContext hook, just pass the context object as an argument.

/src/components/A2.tsx
TSX
1
2
3
4
5
6
7
import { useContext } from 'react' import { ColorContext } from '../App' export default function A2() { const color = useContext(ColorContext) // 'red' return <div style={{ color }}>A2</div> }
/src/components/B1.tsx
TSX
1
2
3
4
5
6
7
8
9
10
import B2 from './B2' export default function B1() { return ( <> <div>B1</div> <B2 /> </> ) }
Nothing to handle
/src/components/B2.tsx
TSX
1
2
3
4
5
6
7
8
9
10
11
12
import B3_1 from './B3_1' import B3_2 from './B3_2' export default function B2() { return ( <> <div>B2</div> <B3_1 /> <B3_2 /> </> ) }
Nothing to handle
/src/components/B3_1.tsx
TSX
1
2
3
export default function B3_1() { return <div>B3_1</div> }
Nothing to handle
/src/components/B3_2.tsx
TSX
1
2
3
4
5
6
7
import { useContext } from 'react' import { ColorContext } from '../App' export default function B3_2() { const color = useContext(ColorContext) // 'red' return <div style={{ color }}>B3_2</div> }

# Modularizing contexts

You can modularize contexts so that the component that provides a context and the components that use it are separated and reusable.

Create a color.tsx file in the /src/providers folder to modularize the context.
Make a ColorProvider component that wraps the content passed in as a slot (children) with the context's .Provider component, and receives and provides the color data.
Then also make a useColor function so that each component can use the provided data more easily.

/src/providers/color.tsx
TSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { createContext, useContext } from 'react' // Create the context export const ColorContext = createContext('') // Provide the context export function ColorProvider({ children, color }: { children: React.ReactNode color: string }) { return ( <ColorContext.Provider value={color}> {children} </ColorContext.Provider> ) } // Use the context export function useColor() { return useContext(ColorContext) }

Note that you may get an ESLint warning or error like the following. This is because when a file mixes component and non-component exports, React Fast Refresh can have trouble telling which parts of the code it should reload.
This can cause problems with hot reloading and state preservation, so you need to split context creation and the provider (component) into separate modules.

1
2
3
Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components. eslint(react-refresh/only-export-components)
/src/providers/color/context.ts
TS
1
2
3
4
5
6
7
8
9
import { createContext, useContext } from 'react' // Create the context export const ColorContext = createContext('') // Use the context export function useColor() { return useContext(ColorContext) }
/src/providers/color/provider.tsx
TSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { ColorContext } from './context' // Provide the context export function ColorProvider({ children, color }: { children: React.ReactNode color: string }) { return ( <ColorContext.Provider value={color}> {children} </ColorContext.Provider> ) }
/src/providers/color/index.ts
TS
1
2
export * from './context' export * from './provider'

Now provide the data in the App component with the modularized ColorProvider component.

/src/App.tsx
TSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { useState } from 'react' import { ColorProvider } from './providers/color' import A1 from './components/A1' import B1 from './components/B1' export default function App() { const [color] = useState('red') return ( <ColorProvider color={color}> <div>App</div> <A1 /> <B1 /> </ColorProvider> ) }

In the components that use the data, use the useColor function instead of the useContext hook as well.

/src/components/A2.tsx
TSX
1
2
3
4
5
6
import { useColor } from '../providers/color' export default function A2() { const color = useColor() // 'red' return <div style={{ color }}>A2</div> }
/src/components/B3_2.tsx
TSX
1
2
3
4
5
6
import { useColor } from '../providers/color' export default function B3_2() { const color = useColor() // 'red' return <div style={{ color }}>B3_2</div> }

# Updating data from descendant components

In the examples so far, the reactive color data is owned by the App component, and the descendant components can only read the data.
If you want descendant components to be able to update the data, you also need to provide a function that updates it.

When we call the useState function, we can get the data and the update function from the returned result with array destructuring assignment.
This time, let's provide this update function along with the data so that descendant components can use it.

First, create the context.
Set the initial value to null, but also create a type for the data you will provide later and specify both as a union type (<ColorState | null>).
Make the data type (ColorState) the same as the return type of the useState function.

Note that because the initial value is null, the return value of the useContext hook can be null.
So you need to use a type guard or a type assertion.

/src/providers/color.tsx
TSX
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
import { createContext, useContext } from 'react' type ColorState = [string, React.Dispatch<React.SetStateAction<string>>] export const ColorContext = createContext<ColorState | null>(null) // Provide the context export function ColorProvider({ children, color }: { children: React.ReactNode color: ColorState }) { return ( <ColorContext.Provider value={color}> {children} </ColorContext.Provider> ) } // Use the context export function useColor() { return useContext(ColorContext)! // Type assertion }

In the App component, provide the return value of useState as is, which includes not only the color value but also the update function.

/src/App.tsx
TSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { useState } from 'react' import { ColorProvider } from './providers/color' import A1 from './components/A1' import B1 from './components/B1' export default function App() { const colorState = useState('red') return ( <ColorProvider color={colorState}> <div>App</div> <A1 /> <B1 /> </ColorProvider> ) }

Now when a descendant component uses the useColor function, it gets both the color value and the update function.
You can change the color by clicking a button, as follows.

/src/components/B3_2.tsx
TSX
1
2
3
4
5
6
7
8
9
10
11
12
13
import { useColor } from '../providers/color' export default function B3_2() { const [color, setColor] = useColor() return ( <> <div style={{ color }}>B3_2</div> <button onClick={() => setColor('blue')}> Change! </button> </> ) }

# Nesting the same context

If needed, you can nest the same context.
When you nest the same context, a descendant component uses the data provided by the closest ancestor component.

Nesting the same context

As we saw in the previous example, the App component provides red through ColorProvider.
Then, as follows, the B1 component, a descendant of App, also uses ColorProvider to provide green.

/src/B1.tsx
TSX
1
2
3
4
5
6
7
8
9
10
11
12
13
import { useState } from 'react' import { ColorProvider } from '../providers/color' import B2 from './B2' export default function B1() { const colorState = useState('green') return ( <ColorProvider color={colorState}> <div>B1</div> <B2 /> </ColorProvider> ) }

Now when the B3_2 component uses the useColor function, it uses the data (green) provided by B1, its closest ancestor component.

/src/components/B3_2.tsx
TSX
1
2
3
4
5
6
7
8
9
10
11
12
13
import { useColor } from '../providers/color' export default function B3_2() { const [color, setColor] = useColor() // 'green' return ( <> <div style={{ color }}>B3_2</div> <button onClick={() => setColor('blue')}> Change! </button> </> ) }

# Using multiple contexts

You can use several contexts in one project.

Create a new count.tsx module as follows.
This module does not receive the data it provides from outside. It manages the data internally.

/src/providers/count.tsx
TSX
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
35
36
import { createContext, useContext, useState } from 'react' type Data = { count: number increase: () => void decrease: () => void } export const CountContext = createContext<Data>({} as Data) // Provide the context export function CountProvider({ children }: { children: React.ReactNode }) { const [count, setCount] = useState(0) const increase = () => setCount(c => c + 1) const decrease = () => setCount(c => c - 1) return ( <CountContext.Provider value={{ count, increase, decrease }}> {children} </CountContext.Provider> ) } // Use the context export function useCount() { return useContext(CountContext) }

In the App component, you can provide the context so that it wraps the descendant components, as follows.
The important point is that ColorProvider and CountProvider are unrelated contexts, so their positions do not matter at all.

/src/App.tsx
TSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { useState } from 'react' import { ColorProvider } from './providers/color' import { CountProvider } from './providers/count' import A1 from './components/A1' import B1 from './components/B1' export default function App() { const colorState = useState('red') return ( <ColorProvider color={colorState}> <CountProvider> <div>App</div> <A1 /> <B1 /> </CountProvider> </ColorProvider> ) }

Then use the provided data appropriately in the descendant components.

/src/components/B3_1.tsx
TSX
1
2
3
4
5
6
7
8
9
10
11
12
13
import { useCount } from '../providers/count' export default function B3_1() { const { count, increase, decrease } = useCount() return ( <> <div>B3_1</div> <div>{count}</div> <button onClick={increase}>+</button> <button onClick={decrease}>-</button> </> ) }

# Consuming a context

We got the context's provided data with the useContext hook, and used it to make and use the useColor and useCount functions.
Another way to get the data is to use the Consumer component.
When you only have the context object, you can get the data directly and render (consume) it with the Consumer component, without importing a separate function.

The content of the Consumer component must be a function, and it receives the provided data as a parameter.

TSX
1
2
3
<MyContext.Consumer> {data => <div>{data}</div>} </MyContext.Consumer>
Consumer component pattern
/src/components/A1.tsx
TSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { ColorContext } from '../providers/color' import { CountContext } from '../providers/count' import A2 from './A2' export default function A1() { return ( <> <ColorContext.Consumer> {colorState => { const [color] = colorState! return <div style={{ color }}>A1</div> }} </ColorContext.Consumer> <CountContext.Consumer> {({ count }) => <div>{count}</div>} </CountContext.Consumer> <A2 /> </> ) }

# Context names

With React Developer Tools, you can check the component structure as follows.
Here, .Provider components use the name Context by default, so it is hard to tell several contexts apart.

No display name

You can name a context with the displayName property of the context object.

/src/providers/color.tsx
TSX
1
2
3
4
// ... export const ColorContext = createContext<ColorState | null>(null) ColorContext.displayName = 'Color'
/src/providers/count.tsx
TSX
1
2
3
4
// ... export const CountContext = createContext<Count>({} as Count) CountContext.displayName = 'Count'

Then the name is shown on the .Provider component, as follows.

Checking the display name