关于 provider-pattern
一项介绍 React Provider Pattern 的技能,用于在组件树中共享数据。内容涵盖 Context、Hooks、Provider 组件及重渲染等权衡。
主要功能
- 01通过 React Context 共享值
- 02使用 useContext 读取值
- 03用自定义 Hook 封装访问
- 04按关注点拆分 Provider
- 05考虑重渲染的影响
适用场景
- 01在组件间共享主题
- 02提供认证或语言环境值
- 03减少多层组件间的属性传递
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ patternsdev/ skills --skill provider-pattern安装后在 Claude Code 中输入/provider-pattern确认已加载。
详细介绍
Provider 模式
目录
在某些情况下,我们希望让应用中的许多(甚至所有)组件都能访问数据。虽然可以使用 props 将数据传递给组件,但如果应用中几乎所有组件都需要访问 props 的值,这种做法就会很麻烦。
我们通常会遇到所谓的 属性逐层传递 问题,也就是将 props 一路向下传递到组件树的深处。依赖这些 props 的代码几乎无法重构,而且很难知道某些数据的来源。
适用场景
- 当许多组件都需要访问相同数据(主题、身份验证、区域设置)时使用
- 当
props需要跨越多个组件层级逐层传递,变得难以维护时,此模式很有帮助
说明
- 使用
React.createContext()创建 Context,并用其 Provider 包裹组件 - 在使用值的组件中使用
useContexthook 访问 Provider 提供的值 - 创建自定义 hook(例如
useThemeContext),封装 Context 的使用逻辑 - 避免对频繁更新的值过度使用 Context,因为值变化时所有使用者都会重新渲染
- 按关注点拆分 Context,以尽量减少不必要的重新渲染
详情
假设我们有一个包含特定数据的 App 组件。在组件树的深处,有一个 ListItem、Header 和 Text 组件,它们都需要这些数据。为了让这些组件获取数据,我们必须让数据经过多层组件传递。
在我们的代码库中,代码大致如下:
function App() {
const data = { ... }
return (
<div>
<SideBar data={data} />
<Content data={data} />
</div>
)
}
const SideBar = ({ data }) => <List data={data} />
const List = ({ data }) => <ListItem data={data} />
const ListItem = ({ data }) => <span>{data.listItem}</span>
const Content = ({ data }) => (
<div>
<Header data={data} />
<Block data={data} />
</div>
)
const Header = ({ data }) => <div>{data.title}</div>
const Block = ({ data }) => <Text data={data} />
const Text = ({ data }) => <h1>{data.text}</h1>
这样逐层传递 props 可能会变得相当混乱。如果将来要重命名 data prop,我们就必须在所有组件中重命名它。应用越大,prop drilling 就越棘手。
如果能跳过那些不需要使用这些数据的组件层级,就再好不过了。我们需要一种方式,让需要访问 data 值的组件能够直接访问它,而不依赖 prop drilling。
这时,Provider 模式就能派上用场!借助 Provider 模式,我们可以让多个组件都能访问数据。我们不必通过 props 逐层传递数据,而是可以用 Provider 包裹所有组件。Provider 是 Context 对象提供给我们的高阶组件。我们可以使用 React 提供的 createContext 方法创建一个 Context 对象。
Provider 接收一个 value prop,其中包含我们想要向下传递的数据。所有被这个 Provider 包裹的组件都能访问 value prop 的值。
const DataContext = React.createContext()
function App() {
const data = { ... }
return (
<div>
<DataContext.Provider value={data}>
<SideBar />
<Content />
</DataContext.Provider>
</div>
)
}
我们不再需要手动将 data prop 逐一传递给每个组件!每个组件都可以通过 useContext Hook 访问 data。这个 Hook 接收 data 所引用的 Context,在本例中是 DataContext。useContext Hook 允许我们读取和写入 Context 对象中的数据。
const DataContext = React.createContext();
function App() {
const data = { ... }
return (
<div>
<DataContext.Provider value={data}>
<SideBar />
<Content />
</DataContext.Provider>
</div>
)
}
const SideBar = () => <List />
const List = () => <ListItem />
const Content = () => <div><Header /><Block /></div>
function ListItem() {
const { data } = React.useContext(DataContext);
return <span>{data.listItem}</span>;
}
function Text() {
const { data } = React.useContext(DataContext);
return <h1>{data.text}</h1>;
}
function Header() {
const { data } = React.useContext(DataContext);
return <div>{data.title}</div>;
}
不使用 data 值的组件完全不必处理 data。我们不再需要担心通过多个不需要这些 props 值的组件层级来传递 props,因此重构会容易得多。
Provider 模式对于共享全局数据非常有用。一个常见的用例是通过 Provider 模式与多个组件共享主题 UI 状态。
假设我们有一个显示列表的简单应用。我们希望用户能够通过切换开关,在浅色模式和深色模式之间切换。当用户从深色模式切换到浅色模式,或反过来切换时,背景颜色和文本颜色都应该变化!我们不必将当前主题值逐一传递给每个组件,而是可以用 ThemeProvider 包裹这些组件,并将当前主题颜色传递给 Provider。
export const ThemeContext = React.createContext();
const themes = {
light: {
background: "#fff",
color: "#000",
},
dark: {
background: "#171717",
color: "#fff",
},
};
export default function App() {
const [theme, setTheme] = useState("dark");
function toggleTheme() {
setTheme(theme === "light" ? "dark" : "light");
}
const providerValue = {
theme: themes[theme],
toggleTheme,
};
return (
<div className={`App theme-${theme}`}>
<ThemeContext.Provider value={providerValue}>
<Toggle />
<List />
</ThemeContext.Provider>
</div>
);
}
由于 Toggle 和 List 组件都被 ThemeContext Provider 包裹,我们可以访问作为 value 传递给 Provider 的 theme 和 toggleTheme 值。
在 Toggle 组件中,我们可以使用 toggleTheme 函数相应地更新主题。
import React, { useContext } from "react";
import { ThemeContext } from "./App";
export default function Toggle() {
const theme = useContext(ThemeContext);
return (
<label className="switch">
<input type="checkbox" onClick={theme.toggleTheme} />
<span className="slider round" />
</label>
);
}
List 组件本身并不关心当前的主题值。不过,ListItem 组件需要!我们可以直接在 ListItem 中使用 theme Context。
import React, { useContext } from "react";
import { ThemeContext } from "./App";
export default function TextBox() {
const theme = useContext(ThemeContext);
return <li style={theme.theme}>...</li>;
}
太好了!我们不必向那些不关心当前主题值的组件传递任何数据。
Hooks
我们可以创建一个 hook,为组件提供上下文。这样一来,不必在每个组件中都导入 useContext 和 Context,而是可以使用一个 hook 来返回所需的上下文。
function useThemeContext() {
const theme = useContext(ThemeContext);
return theme;
}
为了确保主题有效,如果 useContext(ThemeContext) 返回假值,我们就抛出错误。
function useThemeContext() {
const theme = useContext(ThemeContext);
if (!theme) {
throw new Error("useThemeContext must be used within ThemeProvider");
}
return theme;
}
我们不必直接用 ThemeContext.Provider 组件包裹各个组件,而是可以提取出专用的 Provider 组件。这样能将上下文逻辑与渲染组件分离,并提高可复用性。
function ThemeProvider({ children }) {
const [theme, setTheme] = useState("dark");
function toggleTheme() {
setTheme(theme === "light" ? "dark" : "light");
}
const providerValue = {
theme: themes[theme],
toggleTheme,
};
return (
<ThemeContext.Provider value={providerValue}>
{children}
</ThemeContext.Provider>
);
}
export default function App() {
return (
<ThemeProvider>
<div className="App">
<Toggle />
<List />
</div>
</ThemeProvider>
);
}
现在,每个需要访问 ThemeContext 的组件都可以直接使用 useThemeContext hook。
export default function TextBox() {
const theme = useThemeContext();
return <li style={theme.theme}>...</li>;
}
通过为不同的上下文创建 hook,可以轻松地将 Provider 的逻辑与渲染数据的组件分离。
案例分析
有些库提供内置的 Provider,我们可以在使用它们的组件中使用其值。styled-components 就是一个很好的例子。
styled-components 库为我们提供了一个 ThemeProvider。每个 styled component 都可以访问这个 Provider 的值!我们不必自己创建 Context API,可以直接使用现成的!
import { ThemeProvider } from "styled-components";
export default function App() {
const [theme, setTheme] = useState("dark");
function toggleTheme() {
setTheme(theme === "light" ? "dark" : "light");
}
return (
<div className={`App theme-${theme}`}>
<ThemeProvider theme={themes[theme]}>
<Toggle toggleTheme={toggleTheme} />
<List />
</ThemeProvider>
</div>
);
}
我们不再向 ListItem 组件传递内联的 style 属性,而是将它改为 styled.li 组件。由于它是一个 styled component,我们可以访问 theme 的值!
import styled from "styled-components";
export default function ListItem() {
return (
<Li>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
commodo consequat.
</Li>
);
}
const Li = styled.li`
${({ theme }) => `
background-color: ${theme.backgroundColor};
color: ${theme.color};
`}
`;
现在,我们可以使用 ThemeProvider 轻松地为所有 styled components 应用样式!
权衡
优点
Provider 模式/Context API 可以将数据传递给多个组件,而不必手动逐层传递。
它降低了重构代码时意外引入 bug 的风险。以前,如果之后想重命名一个 prop,就必须在整个应用中所有使用该值的地方重命名这个 prop。
我们不再需要处理 prop-drilling,这通常被视为一种反模式。以前,应用的数据流可能很难理解,因为某些 prop 值的来源并不总是清晰。使用 Provider 模式后,我们不必再将 prop 不必要地传递给不关心这些数据的组件。
Provider 模式让维护某种全局状态变得容易,因为我们可以让组件访问该全局状态。
缺点
在某些情况下,过度使用 Provider 模式会导致性能问题。所有 消费 该上下文的组件都会在每次状态变化时重新渲染。
来看一个例子。我们有一个简单的计数器,每次点击 Button 组件中的 Increment 按钮,计数值就会增加。我们还在 Reset 组件中添加了一个 Reset 按钮,用于将计数重置为 0。
由于 Reset 组件使用了 useCountContext,它也会重新渲染。在较小的应用中,这影响不大。但在较大的应用中,将频繁更新的值传递给多个组件可能会对性能产生负面影响。
为了避免组件消费包含不必要且可能会更新的值的 Provider,可以为不同的用例分别创建多个 Provider。