react 中的useContext和Provider实践

1. 为什么需要 Context?(解决什么问题)

在 React 中,数据通常是自上而下通过 props 传递的,这被称为"prop drilling"(属性钻取)。 想象一下这个场景:你的应用有一个主题(比如 lightdark),这个主题需要在很多深层级的组件中使用。

jsx 复制代码
// App.js
function App() {
  const theme = 'dark';
  return <Layout theme={theme} />;
}
// Layout.js
function Layout({ theme }) {
  return <Header theme={theme} />;
}
// Header.js
function Header({ theme }) {
  return <UserAvatar theme={theme} />;
}
// UserAvatar.js
function UserAvatar({ theme }) {
  // 终于用到了!但为了它,theme 穿透了三层不相关的组件。
  return <div className={theme}>Avatar</div>;
}

这种方式的缺点很明显:

  • 代码冗余 :中间层组件(Layout, Header)被迫接收并传递它们自己并不使用的 props
  • 难以维护 :如果需要传递一个新的属性,或者修改属性名,你必须修改所有中间层的组件。 Context 就是解决这个问题的方案 。它提供了一种在组件树中跨层级共享数据的方式,无需手动地逐层传递 props

2. Context 的核心 API 与使用步骤

使用 Context 主要分为三步:

  1. 创建 Context 对象
  2. 提供 Context 值
  3. 消费 Context 值 我们用上面的"主题切换"例子来完整演示。

步骤一:创建 Context

使用 React.createContext 创建一个 Context 对象。这个对象包含两个组件:ProviderConsumer

jsx 复制代码
// ThemeContext.js
import React from 'react';
// 创建一个 Context 对象,并设置一个默认值 'light'
// 当一个组件消费该 Context,但没有找到对应的 Provider 时,就会使用这个默认值。
export const ThemeContext = React.createContext('light');

步骤二:提供 Context 值

使用 ThemeContext.Provider 包裹组件树,并通过 value 属性提供要共享的值。所有被它包裹的后代组件都可以访问到这个值。

jsx 复制代码
// App.js
import React, { useState } from 'react';
import { ThemeContext } from './ThemeContext';
import Toolbar from './Toolbar';
function App() {
  // 使用 state 来管理主题,这样就可以动态切换
  const [theme, setTheme] = useState('dark');
  // Provider 的 value 属性就是我们要共享的值
  return (
    <ThemeContext.Provider value={theme}>
      <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
        Toggle Theme
      </button>
      <Toolbar />
    </ThemeContext.Provider>
  );
}
export default App;

关键点

  • Providervalue 属性是必须的,它决定了后代组件能获取到什么值。
  • value 的值可以是任何类型:字符串、数字、对象、函数等。

步骤三:消费 Context 值

后代组件有两种主要方式来消费 Context 的值。

方式 A:useContext Hook (推荐,最常用)

这是在函数组件中最简洁、最流行的方式。

jsx 复制代码
// ThemedButton.js
import React, { useContext } from 'react';
import { ThemeContext } from './ThemeContext';
function ThemedButton() {
  // 使用 useContext Hook,传入 Context 对象
  // 它会返回离它最近的 Provider 的 value 值
  const theme = useContext(ThemeContext);
  // 根据 theme 值动态设置样式
  const style = {
    background: theme === 'dark' ? '#333' : '#FFF',
    color: theme === 'dark' ? 'white' : 'black',
    padding: '10px 20px',
    border: '1px solid',
  };
  return <button style={style}>I am a {theme} button</button>;
}
export default ThemedButton;
方式 B:Context.Consumer (旧版语法,用于类组件)

useContext Hook 出现之前,这是在函数组件和类组件中消费 Context 的标准方式。现在主要用于类组件。

jsx 复制代码
// ThemedButtonLegacy.js
import React from 'react';
import { ThemeContext } from './ThemeContext';
class ThemedButtonLegacy extends React.Component {
  render() {
    return (
      // Consumer 使用 render props 模式
      // 它需要一个函数作为子元素,这个函数会接收到 context 的 value
      <ThemeContext.Consumer>
        {theme => (
          <button style={{
            background: theme === 'dark' ? '#333' : '#FFF',
            color: theme === 'dark' ? 'white' : 'black',
          }}>
            I am a {theme} button (Legacy)
          </button>
        )}
      </ThemeContext.Consumer>
    );
  }
}
export default ThemedButtonLegacy;

3. 完整示例代码结构

为了让你看得更清楚,这里是一个完整的文件结构:

scss 复制代码
src/
├── App.js              // 提供 Context 的 Provider
├── ThemeContext.js     // 创建并导出 Context
├── Toolbar.js          // 中间层组件,不关心 theme
└── ThemedButton.js     // 消费 Context 的组件

ThemeContext.js

jsx 复制代码
import React from 'react';
export const ThemeContext = React.createContext('light');

App.js

jsx 复制代码
import React, { useState } from 'react';
import { ThemeContext } from './ThemeContext';
import Toolbar from './Toolbar';
function App() {
  const [theme, setTheme] = useState('dark');
  return (
    <ThemeContext.Provider value={theme}>
      <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
        Toggle Theme
      </button>
      <Toolbar />
    </ThemeContext.Provider>
  );
}
export default App;

Toolbar.js

jsx 复制代码
import React from 'react';
import ThemedButton from './ThemedButton';
function Toolbar() {
  // 它不需要知道 theme,直接渲染子组件即可
  return (
    <div>
      <h1>Toolbar</h1>
      <ThemedButton />
    </div>
  );
}
export default Toolbar;

ThemedButton.js

jsx 复制代码
import React, { useContext } from 'react';
import { ThemeContext } from './ThemeContext';
function ThemedButton() {
  const theme = useContext(ThemeContext);
  const style = {
    background: theme === 'dark' ? '#333' : '#FFF',
    color: theme === 'dark' ? 'white' : 'black',
    padding: '10px 20px',
    border: '1px solid',
  };
  return <button style={style}>I am a {theme} button</button>;
}
export default ThemedButton;

4. 最佳实践与注意事项

  1. 将 Provider 和 Consumer 分离

    • 通常,我们会把 createContext 的结果放在一个单独的文件中(如 ThemeContext.js),这样方便在应用的不同地方导入和使用。
  2. 避免滥用 Context

    • Context 主要用于**"全局"**数据,比如主题、用户信息、语言偏好等。
    • 如果只是两三个层级的组件树需要共享数据,使用 props 传递可能更简单、更清晰。滥用 Context 会让组件的复用变得困难,因为组件隐式地依赖了外部的 Context。
  3. Provider 的 value 值变化会触发所有消费者重渲染

    • Providervalue 值发生变化时,所有消费该 Context 的组件都会重新渲染。
    • 性能陷阱 :如果 value 是一个对象或数组,每次渲染都创建一个新的对象/数组,即使内容没变,也会导致消费者组件不必要的重渲染。 错误示范
    jsx 复制代码
    <MyContext.Provider value={{ someValue: 'abc' }}> // 每次都创建新对象!
      <Children />
    </MyContext.Provider>

    正确做法 :将 value 的值提升到 stateuseMemo 中。

    jsx 复制代码
    import React, { useState, useMemo } from 'react';
    function App() {
      const [user, setUser] = useState({ name: 'John' });
      // 使用 useMemo 缓存对象,只有当 user 变化时才创建新对象
      const contextValue = useMemo(() => ({ user, setUser }), [user]);
      return (
        <UserContext.Provider value={contextValue}>
          <Profile />
        </UserContext.Provider>
      );
    }
  4. 一个组件可以消费多个 Context

    jsx 复制代码
    import { useContext } from 'react';
    import { ThemeContext, UserContext } from './contexts';
    function Header() {
      const theme = useContext(ThemeContext);
      const user = useContext(UserContext);
      return <h1 style={{ color: theme.color }}>Welcome, {user.name}</h1>;
    }
相关推荐
青椒a3 小时前
002.nestjs后台管理项目-数据库之prisma(上)
前端
asdfsdgss3 小时前
Angular CDK 自适应布局技巧:响应式工具实操手册
前端·javascript·angular.js
袁煦丞3 小时前
【私人导航员+内网穿透神器】Sun-Panel × cpolar让NAS变身你的数字管家:cpolar内网穿透实验室第564个成功挑战
前端·程序员·远程工作
爱吃的强哥3 小时前
Electron_Vue3 自定义系统托盘及退出二次确认
前端·javascript·electron
袁煦丞3 小时前
开启SSH后,你的NAS竟成私有云“变形金刚”:cpolar内网穿透实验室第645个成功挑战
前端·程序员·远程工作
IT_陈寒3 小时前
SpringBoot 3.2新特性实战:这5个隐藏功能让我开发效率提升50%
前端·人工智能·后端
申阳4 小时前
2小时个人公司:一个全栈开发的精益创业之路
前端·后端·程序员
用户9873824581014 小时前
5. view component
前端
技术小丁4 小时前
零依赖!教你用原生 JS 把 JSON 数组秒变 CSV 文件
前端·javascript