Context
状态提升在共享值的组件彼此靠近时效果很好。但当它们相距很远时就变得很痛苦。一个位于树顶部附近、却需要在下方六层深处使用的值,必须逐级传递给中间的每个组件,而且这些组件都得接收并转发一个它们从不使用的 prop。这种中转过程叫做**prop drilling**,这正是复合组件遇到的问题,用 Children.map 无法解决。Context 是 React 内置的解决方案:一个组件提供一个值,它下方的任何组件都可以直接读取该值,不管中间隔了多少层。
可以把它想象成在传送数据。中间层的组件看不到它,也不用转发它;它们的 prop 列表只需关心自己的事。
创建 context 并提供值
Context 有三个部分:创建它、提供值、读取值。创建发生一次,在文件顶部、任何组件外面。你通常也会导出它,因为读取它的组件往往在其他文件中。
import { createContext } from 'react'
export const ThemeContext = createContext()
export default function App() {
return (
<ThemeContext value="light">
<Header />
<Button />
</ThemeContext>
)
}把 context 对象本身作为组件渲染,使其内部的所有内容都成为等待消费的对象。value prop 是有效载荷:这里是字符串 "light",但可以是任何 JavaScript 值、对象、数组,甚至函数。你放在那里的任何东西,下方的读取者都会收到。这个 prop 必须拼写为 value;这个名字是 API 的一部分。
位置很重要。provider 不一定要包裹整个应用,通常也不应该。把它放在覆盖所有需要该值的组件的最小子树周围。如果只有 UI 的一个角落关心主题,就在该角落的公共祖先处提供主题,让树的其余部分不涉及它。
版本说明
React 19 之前,context 无法直接渲染;你要渲染 <ThemeContext.Provider value="light"> 代替。React 19 让 context 对象本身可以作为 provider 渲染。.Provider 形式仍然有效,是你在大多数现有代码库和课程课时中看到的形式,但 React 计划在未来版本中弃用它,并附带一个代码迁移工具,所以在新代码中使用裸露的 <ThemeContext> 形式。
用 useContext 读取值
provider 下的任何组件都用 useContext 读取值,这是钩子中首次介绍的钩子,传递给它 context 对象,这样 React 就知道要读哪个 context。这就是为什么 context 要被导出。
import { useContext } from 'react'
import { ThemeContext } from './App'
function Header() {
const theme = useContext(ThemeContext)
return (
<header className={`${theme}-theme`}>
<h1>{theme === 'light' ? '亮色' : '暗色'}主题</h1>
</header>
)
}useContext(ThemeContext) 返回这个组件上方最近的 ThemeContext provider 当前持有的值:这里是字符串 "light"。在 App 和 Header 之间没有 prop 传递,在更大树中位于它们之间的组件根本不会提到主题。一个应用可以同时有多个 context,每个都分别创建,每个都通过把自己的对象传递给 useContext 来读取。
让它活起来:状态加 context
硬编码的 value="light" 永远不会改变。让 context 有用的模式是把它和状态配对:状态拥有值并更新它,context 传递它。在对象中传递当前值和改变它的函数,每个消费者都可以从 provider 下方任何地方读取或更新主题。
export const ThemeContext = createContext()
export default function App() {
const [theme, setTheme] = useState('light')
function toggleTheme() {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light')
}
return (
<ThemeContext value={{ theme, toggleTheme }}>
<Header />
<Button />
</ThemeContext>
)
}
function Button() {
const { theme, toggleTheme } = useContext(ThemeContext)
return (
<button className={`${theme}-theme`} onClick={toggleTheme}>
切换主题
</button>
)
}点击按钮时,toggleTheme 运行,App 中的状态更新,App 重新渲染,provider 把新对象传递下去。每个读取 context 的组件都用新值重新渲染。消费者只解构它们需要的属性。
这个作用域技巧在小范围内也有效。像下拉菜单这样的组件可以在自己的子组件周围渲染一个 provider,给其内部的部分一个共享状态,不会泄漏到页面的其余部分。这是复合组件中的菜单的完整版本:
const MenuContext = createContext()
function Menu({ children }) {
const [open, setOpen] = useState(false)
const toggle = () => setOpen(prevOpen => !prevOpen)
return <MenuContext value={{ open, toggle }}>{children}</MenuContext>
}
function MenuButton({ children }) {
const { toggle } = useContext(MenuContext)
return <button onClick={toggle}>{children}</button>
}Menu 拥有 open 并把它和 toggle 都放在 context 上;MenuButton 用 useContext 向上获取 toggle,同样编写的 MenuDropdown 读取 open 来决定是否渲染。中间没有任何东西转发东西,所以调用者可以在按钮外包裹任意多的布局 div,菜单仍然工作。
Context 让靠近顶部的一个组件说"这是值",让下方任何组件用 useContext 直接向上获取它。中间层的组件从不涉及它。
把它和状态配对,值也可以改变:在 provider 中把值和它的更新函数放在一起,任何消费者都可以读取或改变它。
接下来:Render props 和无头组件,一个组件提供行为并让你提供标记。

