Table of Contents

1、概述

在我们前面的学习中,我们知道,我们可以将状态定义在顶层父组件的 useState 中,然后通过 props 传递给子组件。这是非常方便的一种做法。但是,如果此时,我们需要将状态传递给更深层次的子组件时,使用 props 传递虽然也可以实现,但是就显得非常麻烦,并且,这还会破坏组件的语义化:一个不参与自己逻辑的 props 属性需要路过一下

因此,我们需要一种跨组件传递状态的方式,这就是 context。他可以让我们在组件树中,跨层级传递状态,而不需要通过 props 传递。

2、基础语法

我们基于这样一个例子的实现来学习 context 的使用。该案例实现的是一个主题切换功能。我们会在顶层的父组件中,定义一个状态 theme 用于表示当前的主题,然后在点击切换按钮时,更新 theme 状态,并根据 theme 状态来更新页面的主题颜色。

预览
FIG. 01 — TS EDITION
Current Mode: light
主题状态已通过 Context API 注入。当按钮切换时,当前组件的主题颜色,会切换为对应的主题

在该案例中,我们有三个子组件,状态要通过 context 的方式传递给子组件

  1. index.tsx - 顶层的父组件
  2. theme-toggle.tsx - 其中一个子组件:主题切换按钮
  3. theme-display.tsx - 其中一个子组件:内容显示

我们可以使用 createContext 来创建一个 context

code.ts
1
import { createContext } from 'react';
2
3
const ThemeContext = createContext(defaultValue);

当我们在父组件,使用 ThemeContext.Provider 包裹子组件时,我们就可以将 value 属性传递给任意被包裹的子组件。其中,value 属性可以是我们任意想要传递的数据,可以是定义在 useState 中,也可以定义在 useRef

不过由于我们这里需要的是要在状态更新时触发 UI 更新,因此,我们这里需要将状态定义在 useState 中,顶层父组件的代码如下所示

index.tsx
01
import { createContext, useState } from 'react';
02
import ThemeToggle from './theme-toggle';
03
import ThemeDisplay from './theme-display';
04
05
type Theme = 'light' | 'dark';
06
07
export interface ThemeContextType {
08
theme: Theme;
09
toggleTheme: () => void;
10
}
11
12
export const ThemeContext = createContext<ThemeContextType | null>(null);
13
14
export default function ThemeInstance() {
15
const [theme, setTheme] = useState(getSystemTheme);
16
17
const toggleTheme = () => {
18
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
19
};
20
21
const value = { theme, toggleTheme };
22
23
const containerClass = theme === 'dark' ? 'dark bg-gray-950 text-white' : 'bg-white text-black';
24
25
return (
26
<ThemeContext.Provider value={value}>
27
{/* 案例自适应父元素宽高 */}
28
<div className={`p-12 ${containerClass} transition-colors duration-500`}>
29
<div className="flex justify-between items-start border-b border-slate-200 dark:border-neutral-700 pb-8 mb-8">
30
<span className="font-mono text-xs">FIG. 01 — TS EDITION</span>
31
<div className="size-4 bg-current"></div>
32
</div>
33
<ThemeDisplay />
34
35
<div className="flex justify-end pt-8 border-t mt-8 border-slate-200 dark:border-gray-700">
36
<ThemeToggle />
37
</div>
38
</div>
39
</ThemeContext.Provider>
40
);
41
}
42
43
const getSystemTheme = (): Theme => {
44
if (typeof window === 'undefined') return 'light'; // 兼容 SSR
45
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
46
};

在子组件中,我们使用 useContext 来获取 context 从父组件中,传递过来的值。该方法会返回一个对象,其中包含了 context 中传递过来的所有数据。

theme-display.tsx
01
import { useContext } from 'react';
02
import { ThemeContext, ThemeContextType } from './index';
03
04
export default function ThemeDisplay() {
05
const { theme } = useContext(ThemeContext) as ThemeContextType;
06
07
return (
08
<div className="space-y-4">
09
<div className="text-4xl font-black tracking-tighter">
10
Current Mode: <span className="opacity-50 mx-2 uppercase">{theme}</span>
11
</div>
12
<div className="text-sm font-mono opacity-70 leading-relaxed">
13
主题状态已通过 Context API 注入。当按钮切换时,当前组件的主题颜色,会切换为对应的主题
14
</div>
15
</div>
16
);
17
}

在切换按钮组件中,我们还需要拿到 toggleTheme 方法,用于切换状态

theme-toggle.tsx
01
import React, { useContext } from 'react';
02
import { ThemeContext, ThemeContextType } from './index';
03
04
export default function ThemeToggle() {
05
// 使用 'as ThemeContextType' 断言,告诉 TS 我们确信组件包裹在 Provider 内
06
// 这样可以避免每次都检查 if (context === null)
07
const { theme, toggleTheme } = useContext(ThemeContext) as ThemeContextType;
08
09
return (
10
<button
11
onClick={toggleTheme}
12
className={`px-8 py-3 text-sm font-bold transition-colors duration-300 border border-current
13
${theme === 'dark' ? 'hover:bg-white hover:text-black' : 'hover:bg-black hover:text-white'}`}
14
>
15
切换主题
16
</button>
17
);
18
};

3、重新整理代码结构

在上面一个案例中,我们为了演示 context 的使用,他是在顶层父组件中定义的状态,然后通过 contextProvider 组件包裹所有子组件,然后通过 value 属性往下传递参数。

并在子组件中,使用 useContext 来获取 context 中传递过来的值。

在实际的使用中,我们通常会单独将数据部分封装为一个组件,而不会像上面那个例子那样,在 index 中把数据和 UI 在一个组件中混用

因此,我们重新整理代码结构,将数据部分单独封装为一个 ThemeContext.tsx

重新整理后的完整代码如下所示

预览
FIG. 01 — TS EDITION
Current Mode: light
主题状态已通过 Context API 注入。当按钮切换时,当前组件的主题颜色,会切换为对应的主题
index.tsx
theme-context.tsx
theme-display.tsx
theme-toggle.tsx
01
import { useContext } from 'react';
02
import ThemeToggle from './theme-toggle';
03
import ThemeDisplay from './theme-display';
04
import ThemeContextProvider, { ThemeContext } from './theme-context';
05
06
07
export default function App() {
08
const { theme } = useContext(ThemeContext);
09
const containerClass = theme === 'dark' ? 'dark bg-gray-950 text-white' : 'bg-white text-black';
10
11
return (
12
<ThemeContextProvider>
13
{/* 案例自适应父元素宽高 */}
14
<div className={`p-12 ${containerClass} transition-colors duration-500`}>
15
<div className="flex justify-between items-start border-b border-slate-200 dark:border-neutral-700 pb-8 mb-8">
16
<span className="font-mono text-xs">FIG. 01 — TS EDITION</span>
17
<div className="size-4 bg-current"></div>
18
</div>
19
<ThemeDisplay />
20
21
<div className="flex justify-end pt-8 border-t mt-8 border-slate-200 dark:border-gray-700">
22
<ThemeToggle />
23
</div>
24
</div>
25
</ThemeContextProvider>
26
);
27
}
28
专栏首页
到顶
专栏目录