在我们前面的学习中,我们知道,我们可以将状态定义在顶层父组件的 useState 中,然后通过 props 传递给子组件。这是非常方便的一种做法。但是,如果此时,我们需要将状态传递给更深层次的子组件时,使用 props 传递虽然也可以实现,但是就显得非常麻烦,并且,这还会破坏组件的语义化:一个不参与自己逻辑的 props 属性需要路过一下
因此,我们需要一种跨组件传递状态的方式,这就是 context。他可以让我们在组件树中,跨层级传递状态,而不需要通过 props 传递。
我们基于这样一个例子的实现来学习 context 的使用。该案例实现的是一个主题切换功能。我们会在顶层的父组件中,定义一个状态 theme 用于表示当前的主题,然后在点击切换按钮时,更新 theme 状态,并根据 theme 状态来更新页面的主题颜色。
在该案例中,我们有三个子组件,状态要通过 context 的方式传递给子组件
我们可以使用 createContext 来创建一个 context
1import { createContext } from 'react';23const ThemeContext = createContext(defaultValue);
当我们在父组件,使用 ThemeContext.Provider 包裹子组件时,我们就可以将 value 属性传递给任意被包裹的子组件。其中,value 属性可以是我们任意想要传递的数据,可以是定义在 useState 中,也可以定义在 useRef 中
不过由于我们这里需要的是要在状态更新时触发 UI 更新,因此,我们这里需要将状态定义在 useState 中,顶层父组件的代码如下所示
01import { createContext, useState } from 'react';02import ThemeToggle from './theme-toggle';03import ThemeDisplay from './theme-display';0405type Theme = 'light' | 'dark';0607export interface ThemeContextType {08theme: Theme;09toggleTheme: () => void;10}1112export const ThemeContext = createContext<ThemeContextType | null>(null);1314export default function ThemeInstance() {15const [theme, setTheme] = useState(getSystemTheme);1617const toggleTheme = () => {18setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));19};2021const value = { theme, toggleTheme };2223const containerClass = theme === 'dark' ? 'dark bg-gray-950 text-white' : 'bg-white text-black';2425return (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 />3435<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}4243const getSystemTheme = (): Theme => {44if (typeof window === 'undefined') return 'light'; // 兼容 SSR45return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';46};
在子组件中,我们使用 useContext 来获取 context 从父组件中,传递过来的值。该方法会返回一个对象,其中包含了 context 中传递过来的所有数据。
01import { useContext } from 'react';02import { ThemeContext, ThemeContextType } from './index';0304export default function ThemeDisplay() {05const { theme } = useContext(ThemeContext) as ThemeContextType;0607return (08<div className="space-y-4">09<div className="text-4xl font-black tracking-tighter">10Current 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 方法,用于切换状态
01import React, { useContext } from 'react';02import { ThemeContext, ThemeContextType } from './index';0304export default function ThemeToggle() {05// 使用 'as ThemeContextType' 断言,告诉 TS 我们确信组件包裹在 Provider 内06// 这样可以避免每次都检查 if (context === null)07const { theme, toggleTheme } = useContext(ThemeContext) as ThemeContextType;0809return (10<button11onClick={toggleTheme}12className={`px-8 py-3 text-sm font-bold transition-colors duration-300 border border-current13${theme === 'dark' ? 'hover:bg-white hover:text-black' : 'hover:bg-black hover:text-white'}`}14>15切换主题16</button>17);18};
在上面一个案例中,我们为了演示 context 的使用,他是在顶层父组件中定义的状态,然后通过 context 的 Provider 组件包裹所有子组件,然后通过 value 属性往下传递参数。
并在子组件中,使用 useContext 来获取 context 中传递过来的值。
在实际的使用中,我们通常会单独将数据部分封装为一个组件,而不会像上面那个例子那样,在 index 中把数据和 UI 在一个组件中混用
因此,我们重新整理代码结构,将数据部分单独封装为一个 ThemeContext.tsx
重新整理后的完整代码如下所示
01import { useContext } from 'react';02import ThemeToggle from './theme-toggle';03import ThemeDisplay from './theme-display';04import ThemeContextProvider, { ThemeContext } from './theme-context';050607export default function App() {08const { theme } = useContext(ThemeContext);09const containerClass = theme === 'dark' ? 'dark bg-gray-950 text-white' : 'bg-white text-black';1011return (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 />2021<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