provider-pattern
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseProvider Pattern
Provider模式
In some cases, we want to make available data to many (if not all) components in an application. Although we can pass data to components using , this can be difficult to do if almost all components in your application need access to the value of the props.
propsWe often end up with something called prop drilling, which is the case when we pass props far down the component tree. Refactoring the code that relies on the props becomes almost impossible, and knowing where certain data comes from is difficult.
在某些场景下,我们希望应用中的多个(甚至所有)组件都能访问到特定数据。虽然我们可以通过向组件传递数据,但如果应用中几乎所有组件都需要访问这些属性值,这种方式就会变得非常繁琐。
props我们常常会遇到所谓的prop drilling(属性透传)问题:即需要将props沿着组件树逐层向下传递。此时,重构依赖这些props的代码几乎变得不可能,而且很难追踪某些数据的来源。
When to Use
适用场景
- Use this when many components need access to the same data (themes, auth, locale)
- This is helpful when prop drilling becomes unwieldy across multiple component layers
- 当多个组件需要访问同一数据时(如主题、鉴权信息、语言区域设置)
- 当prop drilling在多层组件间变得难以维护时
Instructions
实现步骤
- Create a Context with and wrap components with its Provider
React.createContext() - Use the hook in consuming components to access provided values
useContext - Create custom hooks (e.g., ) to encapsulate context consumption logic
useThemeContext - Avoid overusing context for frequently updated values as all consumers re-render on change
- Split contexts by concern to minimize unnecessary re-renders
- 使用创建Context,并使用其Provider包裹组件
React.createContext() - 在消费组件中使用钩子来访问Provider提供的值
useContext - 创建自定义钩子(如)来封装Context的消费逻辑
useThemeContext - 避免将频繁更新的值放入Context,因为所有消费组件都会在值变化时重新渲染
- 按关注点拆分Context,以减少不必要的重新渲染
Details
详细说明
Let's say that we have one component that contains certain data. Far down the component tree, we have a , and component that all need this data. In order to get this data to these components, we'd have to pass it through multiple layers of components.
AppListItemHeaderTextIn our codebase, that would look something like the following:
js
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>Passing props down this way can get quite messy. If we want to rename the prop in the future, we'd have to rename it in all components. The bigger your application gets, the trickier prop drilling can be.
dataIt would be optimal if we could skip all the layers of components that don't need to use this data. We need to have something that gives the components that need access to the value of direct access to it, without relying on prop drilling.
dataThis is where the Provider Pattern can help us out! With the Provider Pattern, we can make data available to multiple components. Rather than passing that data down each layer through props, we can wrap all components in a . A Provider is a higher order component provided to us by the object. We can create a Context object, using the method that React provides for us.
ProviderContextcreateContextThe Provider receives a prop, which contains the data that we want to pass down. All components that are wrapped within this provider have access to the value of the prop.
valuevaluejs
const DataContext = React.createContext()
function App() {
const data = { ... }
return (
<div>
<DataContext.Provider value={data}>
<SideBar />
<Content />
</DataContext.Provider>
</div>
)
}We no longer have to manually pass down the prop to each component! Each component can get access to the , by using the hook. This hook receives the context that has a reference with, in this case. The hook lets us read and write data to the context object.
datadatauseContextdataDataContextuseContextjs
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>;
}The components that aren't using the value won't have to deal with at all. We no longer have to worry about passing props down several levels through components that don't need the value of the props, which makes refactoring a lot easier.
datadataThe Provider pattern is very useful for sharing global data. A common usecase for the provider pattern is sharing a theme UI state with many components.
Say we have a simple app that shows a list. We want the user to be able to switch between lightmode and darkmode, by toggling the switch. When the user switches from dark- to lightmode and vice versa, the background color and text color should change! Instead of passing the current theme value down to each component, we can wrap the components in a , and pass the current theme colors to the provider.
ThemeProviderjs
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>
);
}Since the and components are both wrapped within the provider, we have access to the values and that are passed as a to the provider.
ToggleListThemeContextthemetoggleThemevalueWithin the component, we can use the function to update the theme accordingly.
ToggletoggleThemejs
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>
);
}The component itself doesn't care about the current value of the theme. However, the components do! We can use the context directly within the .
ListListItemthemeListItemjs
import React, { useContext } from "react";
import { ThemeContext } from "./App";
export default function TextBox() {
const theme = useContext(ThemeContext);
return <li style={theme.theme}>...</li>;
}Perfect! We didn't have to pass down any data to components that didn't care about the current value of the theme.
假设我们有一个组件,它包含了一些特定数据。在组件树的深层,有、和组件都需要访问这些数据。为了将数据传递给这些组件,我们必须逐层通过多个组件进行传递。
AppListItemHeaderText在代码中,这看起来如下所示:
js
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的方式会变得非常混乱。如果未来我们想要重命名这个prop,就必须在所有使用它的组件中逐一修改。应用规模越大,prop drilling的问题就越棘手。
data理想情况下,我们可以跳过所有不需要使用这些数据的组件层。我们需要一种机制,让需要访问值的组件能够直接获取它,而无需依赖prop drilling。
data这就是Provider模式能帮我们解决的问题!通过Provider模式,我们可以让多个组件访问同一数据。无需通过props逐层传递数据,我们可以用包裹所有组件。Provider是对象提供的高阶组件。我们可以使用React提供的方法创建一个Context对象。
ProviderContextcreateContextProvider接收一个属性,其中包含我们想要传递的数据。所有被该Provider包裹的组件都可以访问属性的值。
valuevaluejs
const DataContext = React.createContext()
function App() {
const data = { ... }
return (
<div>
<DataContext.Provider value={data}>
<SideBar />
<Content />
</DataContext.Provider>
</div>
)
}我们不再需要手动将prop传递给每个组件!每个组件都可以通过钩子来获取。这个钩子接收与关联的Context(在本例中是)。钩子允许我们读取和写入Context对象中的数据。
datauseContextdatadataDataContextuseContextjs
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>;
}那些不使用值的组件完全不需要处理相关逻辑。我们再也不用担心将props传递给不需要这些值的多层组件,这让重构工作变得容易得多。
datadataProvider模式非常适合共享全局数据。一个常见的应用场景是与多个组件共享主题UI状态。
假设我们有一个展示列表的简单应用。我们希望用户能够通过切换开关在亮色模式和暗色模式之间切换。当用户在两种模式间切换时,背景色和文字色应该随之改变!我们无需将当前主题值传递给每个组件,而是可以用包裹组件,并将当前主题颜色传递给Provider。
ThemeProviderjs
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>
);
}由于和组件都被包裹在Provider中,我们可以访问作为传递给Provider的和值。
ToggleListThemeContextvaluethemetoggleTheme在组件中,我们可以使用函数来相应地更新主题。
ToggletoggleThemejs
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>
);
}ListListItemListItemthemejs
import React, { useContext } from "react";
import { ThemeContext } from "./App";
export default function TextBox() {
const theme = useContext(ThemeContext);
return <li style={theme.theme}>...</li>;
}完美!我们无需将数据传递给那些不关心当前主题值的组件。
Hooks
钩子(Hooks)
We can create a hook to provide context to components. Instead of having to import and the Context in each component, we can use a hook that returns the context we need.
useContextjs
function useThemeContext() {
const theme = useContext(ThemeContext);
return theme;
}To make sure that it's a valid theme, let's throw an error if returns a falsy value.
useContext(ThemeContext)js
function useThemeContext() {
const theme = useContext(ThemeContext);
if (!theme) {
throw new Error("useThemeContext must be used within ThemeProvider");
}
return theme;
}Instead of wrapping the components directly with the component, we can extract a dedicated provider component. This keeps the context logic separate from the rendering components and improves reusability.
ThemeContext.Providerjs
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>
);
}Each component that needs to have access to the , can now simply use the hook.
ThemeContextuseThemeContextjs
export default function TextBox() {
const theme = useThemeContext();
return <li style={theme.theme}>...</li>;
}By creating hooks for the different contexts, it's easy to separate the providers's logic from the components that render the data.
我们可以创建一个钩子来为组件提供Context。无需在每个组件中都导入和Context,我们可以使用一个钩子来返回所需的Context。
useContextjs
function useThemeContext() {
const theme = useContext(ThemeContext);
return theme;
}为了确保主题有效,如果返回假值,我们可以抛出一个错误。
useContext(ThemeContext)js
function useThemeContext() {
const theme = useContext(ThemeContext);
if (!theme) {
throw new Error("useThemeContext must be used within ThemeProvider");
}
return theme;
}我们可以提取一个专门的Provider组件,而不是直接用组件包裹其他组件。这可以将Context逻辑与渲染组件分离,提高复用性。
ThemeContext.Providerjs
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>
);
}现在,每个需要访问的组件都可以简单地使用钩子。
ThemeContextuseThemeContextjs
export default function TextBox() {
const theme = useThemeContext();
return <li style={theme.theme}>...</li>;
}通过为不同的Context创建钩子,我们可以轻松地将Provider的逻辑与渲染数据的组件分离。
Case Study
案例研究
Some libraries provide built-in providers, which values we can use in the consuming components. A good example of this, is styled-components.
No experience with styled-components is needed to understand this example.
The styled-components library provides a for us. Each styled component will have access to the value of this provider! Instead of creating a context API ourselves, we can use the one that's been provided to us!
ThemeProviderjs
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>
);
}Instead of passing an inline prop to the component, we'll make it a component. Since it's a styled component, we can access the value of !
styleListItemstyled.lithemejs
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};
`}
`;We can now easily apply styles to all our styled components with the !
ThemeProvider一些库提供了内置的Provider,我们可以在消费组件中使用它们的值。styled-components就是一个很好的例子。
理解这个示例不需要具备styled-components的使用经验。
styled-components库为我们提供了一个。每个styled组件都可以访问这个Provider的值!我们无需自己创建Context API,直接使用库提供的即可!
ThemeProviderjs
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>
);
}我们不再需要向组件传递内联的属性,而是将它变成一个组件。由于它是一个styled组件,我们可以访问的值!
ListItemstylestyled.lithemejs
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};
`}
`;现在,我们可以通过轻松地为所有styled组件应用样式!
ThemeProviderTradeoffs
权衡
Pros
优点
The Provider pattern/Context API makes it possible to pass data to many components, without having to manually pass it through each component layer.
It reduces the risk of accidentally introducing bugs when refactoring code. Previously, if we later on wanted to rename a prop, we had to rename this prop throughout the entire application where this value was used.
We no longer have to deal with prop-drilling, which could be seen as an anti-pattern. Previously, it could be difficult to understand the dataflow of the application, as it wasn't always clear where certain prop values originated. With the Provider pattern, we no longer have to unnecessarily pass props to component that don't care about this data.
Keeping some sort of global state is made easy with the Provider pattern, as we can give components access to this global state.
Provider模式/Context API可以在无需逐层手动传递数据的情况下,为多个组件提供数据访问权限。
它降低了重构代码时意外引入bug的风险。以前,如果我们想要重命名一个prop,必须在整个应用中所有使用该值的地方逐一修改。
我们不再需要处理被视为反模式的prop-drilling问题。以前,应用的数据流向可能难以理解,因为某些prop值的来源并不总是清晰的。使用Provider模式后,我们无需再将props不必要地传递给不关心这些数据的组件。
Provider模式让维护全局状态变得容易,因为我们可以让组件访问这个全局状态。
Cons
缺点
In some cases, overusing the Provider pattern can result in performance issues. All components that consume the context re-render on each state change.
Let's look at an example. We have a simple counter which value increases every time we click on the button in the component. We also have a button in the component, which resets the count back to .
IncrementButtonResetReset0The component also re-rendered since it consumed the . In smaller applications, this won't matter too much. In larger applications, passing a frequently updated value to many components can affect the performance negatively.
ResetuseCountContextTo make sure that components aren't consuming providers that contain unnecessary values which may update, you can create several providers for each separate usecase.
在某些情况下,过度使用Provider模式可能会导致性能问题。所有消费Context的组件都会在状态变化时重新渲染。
让我们看一个例子。我们有一个简单的计数器,每次点击组件中的按钮,计数器的值就会增加。我们还有一个组件中的按钮,可以将计数重置为。
ButtonIncrementResetReset0由于组件也消费了,它也会重新渲染。在小型应用中,这不会有太大影响。但在大型应用中,将频繁更新的值传递给多个组件可能会对性能产生负面影响。
ResetuseCountContext为了避免组件消费包含不必要的、可能会更新的值的Provider,你可以为每个不同的使用场景创建多个Provider。