🎯 React 组件通信方式概览
在 React 开发中,组件之间的数据传递是核心问题。目前主要有四种通信方式:
1️⃣ 父子组件通信
这是最基础的通信方式,父组件通过 props 将数据传递给子组件。这种方式简单直接,适用于层级较浅的组件关系。
jsx
function Child({ message }) {
return <div>{message}</div>
}
function Parent() {
return <Child message="Hello from Parent" />
}
2️⃣ 子父组件通信
子组件通过回调函数将数据传递给父组件。父组件定义一个函数,通过 props 传递给子组件,子组件调用这个函数来传递数据。
jsx
function Child({ onMessage }) {
return <button onClick={() => onMessage('Hello from Child')}>发送消息</button>
}
function Parent() {
const handleMessage = (msg) => {
console.log(msg)
}
return <Child onMessage={handleMessage} />
}
3️⃣ 兄弟组件通信
兄弟组件之间的通信通常需要通过共同的父组件作为中转。父组件维护共享状态,兄弟组件通过 props 接收和修改这个状态。
jsx
function SiblingA({ value, onChange }) {
return <input value={value} onChange={(e) => onChange(e.target.value)} />
}
function SiblingB({ value }) {
return <div>输入的值:{value}</div>
}
function Parent() {
const [value, setValue] = useState('')
return (
<>
<SiblingA value={value} onChange={setValue} />
<SiblingB value={value} />
</>
)
}
4️⃣ 跨层级通信
当组件层级较深时,使用 props 一层层传递数据会变得非常繁琐。这就是 Context 要解决的问题。
🚀 Context 的诞生背景
痛点分析
在传统的父子组件通信中,如果需要将数据从顶层组件传递到深层嵌套的子组件,就必须经过每一层中间组件。这种方式存在以下问题:
传递路径过长:数据需要经过多个中间组件,每个组件都需要接收并传递 props,即使它们并不使用这些数据。
维护成本高:每次修改数据结构或添加新的 props,都需要修改整条传递链路上的所有组件。
代码冗余:中间组件充满了不相关的 props 传递,代码可读性降低。
现实类比
这就像古代的驿站传递制度:皇帝要给边疆的将军送一封密信,必须经过沿途的每个驿站,每个驿站都要接收并转发这封信,即使驿站官员并不关心信的内容。这种传递方式效率低下,而且容易在传递过程中出现问题。
就像《长安的荔枝》中描述的那样,为了将新鲜荔枝从岭南送到长安,需要经过无数驿站,每个驿站都要接力传递,成本极高,风险很大。
💡 Context 的核心思想
Context 提供了一种在组件树中共享数据的方式,无需通过 props 一层层传递。它的核心思想是:
数据在查找的上下文里:在最外层组件提供数据,任何层级的组件都可以直接访问这些数据。
主动查找能力:需要消费数据的组件拥有主动查找数据的能力,而不是被动接收。
规矩不变:父组件(外层组件)仍然负责持有和改变数据,只是传递方式从"一路传"变成了"全局提供"。
📖 Context 的三步使用法
第一步:创建 Context 容器
使用 createContext 创建一个 Context 对象,这个对象就是数据容器。可以传入一个默认值作为参数。
jsx
import { createContext } from 'react'
export const UserContext = createContext(null)
createContext 接受一个默认值参数,当组件在匹配的 Provider 之外使用 Context 时,会使用这个默认值。
第二步:提供数据
使用 Context.Provider 组件包裹需要共享数据的组件树,通过 value 属性提供数据。
jsx
export default function App() {
const user = {
name: "Andrew"
}
return (
<UserContext.Provider value={user}>
<Page />
</UserContext.Provider>
)
}
Provider 组件接受一个 value 属性,这个值会被所有消费该 Context 的组件访问到。Provider 可以嵌套使用,内层的 Provider 会覆盖外层的值。
第三步:消费数据
使用 useContext Hook 在组件中消费 Context 数据。
jsx
import { useContext } from 'react'
import { UserContext } from '../App'
export default function UserInfo() {
const user = useContext(UserContext)
return (
<div>{user.name}</div>
)
}
useContext 接受一个 Context 对象作为参数,返回该 Context 的当前值。当 Context 的值发生变化时,使用该 Context 的组件会重新渲染。
🎨 实战案例:主题切换应用
需求分析
创建一个支持白天/夜间主题切换的应用,主题状态需要在整个应用中共享。这是一个典型的跨层级通信场景。
项目结构
css
theme-demo/
├── src/
│ ├── App.jsx
│ ├── contexts/
│ │ └── ThemeContext.jsx
│ ├── components/
│ │ ├── Header.jsx
│ │ └── Content.jsx
│ ├── pages/
│ │ └── Page.jsx
│ └── theme.css
创建 ThemeContext
首先创建主题 Context,包含主题状态和切换主题的方法。
jsx
import { createContext, useState, useEffect } from 'react'
export const ThemeContext = createContext(null)
export default function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light')
const toggleTheme = () => {
setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'))
}
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme)
document.body.setAttribute('data-theme', theme)
}, [theme])
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}
关键点解析:
useState管理主题状态,初始值为 'light'toggleTheme函数使用函数式更新,确保基于前一个状态进行切换useEffect监听主题变化,同步更新html和body元素的data-theme属性ThemeProvider作为高阶组件,包裹子组件并提供主题上下文
在应用中使用 ThemeProvider
在应用的根组件中使用 ThemeProvider 包裹整个组件树。
jsx
import ThemeProvider from './contexts/ThemeContext'
import Page from './pages/Page'
export default function App() {
return (
<>
<ThemeProvider>
<Page />
</ThemeProvider>
</>
)
}
在组件中消费主题数据
在 Header 组件中消费主题数据,显示当前主题并提供切换按钮。
jsx
import { useContext } from 'react'
import { ThemeContext } from '../contexts/ThemeContext'
export default function Header() {
const { theme, toggleTheme } = useContext(ThemeContext)
return (
<div style={{ padding: 24 }}>
<h2>当前主题:{theme}</h2>
<button className="button" onClick={toggleTheme}>
切换主题
</button>
</div>
{
在 Page 组件中也可以消费主题数据,实现不同组件共享同一主题状态。
jsx
import { useContext } from 'react'
import { ThemeContext } from '../contexts/ThemeContext'
import Header from '../components/Header'
import Content from '../components/Content'
export default function Page() {
const theme = useContext(ThemeContext)
return (
<div style={{ padding: 24 }}>
<Header />
<Content />
</div>
)
}
🎯 CSS 变量实现主题切换
使用 CSS 变量(Custom Properties)是实现主题切换的优雅方式。CSS 变量允许我们在不同的主题下动态改变样式。
定义 CSS 变量
在 theme.css 中定义主题相关的 CSS 变量。
css
:root {
--bg-color: pink
--text-color: #222
--primary-color: #1677ff
}
[data-theme='dark'] {
--bg-color: #141414
--text-color: #f5f5f5
--primary-color: #4e8cff
}
语法解析:
:root选择器匹配文档的根元素,在这里定义全局 CSS 变量--bg-color、--text-color等是自定义属性名,必须以--开头[data-theme='dark']是属性选择器,匹配所有data-theme属性值为'dark'的元素- 在不同的选择器中重新定义变量值,实现主题切换
使用 CSS 变量
在样式中使用 var() 函数引用 CSS 变量。
css
body {
margin: 0
background-color: var(--bg-color)
color: var(--text-color)
transition: all 0.3s
}
.button {
padding: 8px 16px
background: var(--primary-color)
color: #fff
border: none
cursor: pointer
}
关键特性:
var(--bg-color)引用之前定义的 CSS 变量- 当
data-theme属性改变时,CSS 变量的值会自动更新 transition: all 0.3s实现主题切换的平滑过渡效果
JavaScript 控制 CSS 变量
通过 JavaScript 动态修改元素的 data-theme 属性,触发 CSS 变量的变化。
jsx
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme)
document.body.setAttribute('data-theme', theme)
}, [theme])
当 theme 状态变化时,useEffect 会执行,更新 DOM 元素的属性,从而触发 CSS 变量的重新计算。
🔍 Context 的高级特性
默认值的作用
Context 的默认值在以下情况下使用:
- 组件在匹配的 Provider 之外使用 Context
- Provider 的 value 属性为 undefined
jsx
const MyContext = createContext('默认值')
function Component() {
const value = useContext(MyContext)
return <div>{value}</div>
}
export default function App() {
return <Component />
}
在这个例子中,Component 会显示"默认值",因为它没有被任何 Provider 包裹。
Provider 嵌套
多个 Provider 可以嵌套使用,内层的 Provider 会覆盖外层的值。
jsx
const ThemeContext = createContext('light')
const ColorContext = createContext('blue')
function Child() {
const theme = useContext(ThemeContext)
const color = useContext(ColorContext)
return <div>主题:{theme},颜色:{color}</div>
}
export default function App() {
return (
<ThemeContext.Provider value="dark">
<ColorContext.Provider value="red">
<Child />
</ColorContext.Provider>
</ThemeContext.Provider>
)
}
Child 组件会显示"主题:dark,颜色:red"。
Context 性能优化
当 Context 的值变化时,所有消费该 Context 的组件都会重新渲染。为了优化性能,可以:
- 拆分 Context:将频繁变化和不常变化的数据拆分到不同的 Context 中
jsx
const UserContext = createContext(null)
const ThemeContext = createContext(null)
function App() {
const [user, setUser] = useState(null)
const [theme, setTheme] = useState('light')
return (
<UserContext.Provider value={user}>
<ThemeContext.Provider value={theme}>
<Child />
</ThemeContext.Provider>
</UserContext.Provider>
)
}
- 使用 useMemo:避免不必要的对象重新创建
jsx
function App() {
const [theme, setTheme] = useState('light')
const contextValue = useMemo(() => ({
theme,
toggleTheme: () => setTheme(prev => prev === 'light' ? 'dark' : 'light')
}), [theme])
return (
<ThemeContext.Provider value={contextValue}>
<Child />
</ThemeContext.Provider>
)
}
- 使用 React.memo:避免不必要的组件重新渲染
jsx
const ExpensiveComponent = React.memo(function ExpensiveComponent() {
const { theme } = useContext(ThemeContext)
return <div>主题:{theme}</div>
})
📝 Context 最佳实践
1. 合理拆分 Context
不要将所有数据都放在一个 Context 中,应该根据功能模块合理拆分。
jsx
const UserContext = createContext(null)
const ThemeContext = createContext(null)
const LocaleContext = createContext(null)
2. 使用自定义 Hook
封装 Context 的消费逻辑,提供更友好的 API。
jsx
export function useTheme() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme 必须在 ThemeProvider 内部使用')
}
return context
}
function Component() {
const { theme, toggleTheme } = useTheme()
return <button onClick={toggleTheme}>{theme}</button>
}
3. 提供默认值或错误处理
确保 Context 的使用是安全的,提供合理的默认值或错误处理。
。
jsx
export function useUser() {
const user = useContext(UserContext)
if (!user) {
throw new Error('useUser 必须在 UserProvider 内部使用')
}
return user
}
4. 避免过度使用
Context 适合用于全局状态,如用户信息、主题、语言设置等。对于局部状态,仍然应该使用 props 或状态管理库。
5. 文档化 Context
为 Context 添加清晰的文档说明,包括提供的数据类型和使用方法。
jsx
/**
* 用户上下文
* @type {React.Context<{
* name: string
* email: string
* role: string
* }>}
*/
export const UserContext = createContext(null)
🌟 总结
React Context 是解决跨层级组件通信的强大工具,它提供了优雅的数据共享方式,避免了 props 传递的繁琐。通过合理使用 Context,可以:
- 简化组件间的数据传递
- 提高代码的可维护性
- 实现全局状态管理
- 构建更灵活的应用架构
结合 CSS 变量,Context 可以轻松实现主题切换、国际化等全局功能。在实际开发中,应该根据具体需求选择合适的通信方式,Context 是工具箱中的重要一员,但不是唯一的解决方案。