provider-pattern

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Provider 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
props
, this can be difficult to do if almost all components in your application need access to the value of the props.
We 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
    React.createContext()
    and wrap components with its Provider
  • Use the
    useContext
    hook in consuming components to access provided values
  • Create custom hooks (e.g.,
    useThemeContext
    ) to encapsulate context consumption logic
  • Avoid overusing context for frequently updated values as all consumers re-render on change
  • Split contexts by concern to minimize unnecessary re-renders
  • 使用
    React.createContext()
    创建Context,并使用其Provider包裹组件
  • 在消费组件中使用
    useContext
    钩子来访问Provider提供的值
  • 创建自定义钩子(如
    useThemeContext
    )来封装Context的消费逻辑
  • 避免将频繁更新的值放入Context,因为所有消费组件都会在值变化时重新渲染
  • 按关注点拆分Context,以减少不必要的重新渲染

Details

详细说明

Let's say that we have one
App
component that contains certain data. Far down the component tree, we have a
ListItem
,
Header
and
Text
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.
In 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
data
prop in the future, we'd have to rename it in all components. The bigger your application gets, the trickier prop drilling can be.
It 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
data
direct access to it, without relying on prop drilling.
This 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
Provider
. A Provider is a higher order component provided to us by the
Context
object. We can create a Context object, using the
createContext
method that React provides for us.
The Provider receives a
value
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
value
prop.
js
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
data
prop to each component! Each component can get access to the
data
, by using the
useContext
hook. This hook receives the context that
data
has a reference with,
DataContext
in this case. The
useContext
hook lets us read and write data to the context object.
js
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
data
value won't have to deal with
data
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.
The 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
ThemeProvider
, and pass the current theme colors to the provider.
js
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
Toggle
and
List
components are both wrapped within the
ThemeContext
provider, we have access to the values
theme
and
toggleTheme
that are passed as a
value
to the provider.
Within the
Toggle
component, we can use the
toggleTheme
function to update the theme accordingly.
js
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
List
component itself doesn't care about the current value of the theme. However, the
ListItem
components do! We can use the
theme
context directly within the
ListItem
.
js
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.
假设我们有一个
App
组件,它包含了一些特定数据。在组件树的深层,有
ListItem
Header
Text
组件都需要访问这些数据。为了将数据传递给这些组件,我们必须逐层通过多个组件进行传递。
在代码中,这看起来如下所示:
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的方式会变得非常混乱。如果未来我们想要重命名
data
这个prop,就必须在所有使用它的组件中逐一修改。应用规模越大,prop drilling的问题就越棘手。
理想情况下,我们可以跳过所有不需要使用这些数据的组件层。我们需要一种机制,让需要访问
data
值的组件能够直接获取它,而无需依赖prop drilling。
这就是Provider模式能帮我们解决的问题!通过Provider模式,我们可以让多个组件访问同一数据。无需通过props逐层传递数据,我们可以用
Provider
包裹所有组件。Provider是
Context
对象提供的高阶组件。我们可以使用React提供的
createContext
方法创建一个Context对象。
Provider接收一个
value
属性,其中包含我们想要传递的数据。所有被该Provider包裹的组件都可以访问
value
属性的值。
js
const DataContext = React.createContext()

function App() {
  const data = { ... }

  return (
    <div>
      <DataContext.Provider value={data}>
        <SideBar />
        <Content />
      </DataContext.Provider>
    </div>
  )
}
我们不再需要手动将
data
prop传递给每个组件!每个组件都可以通过
useContext
钩子来获取
data
。这个钩子接收与
data
关联的Context(在本例中是
DataContext
)。
useContext
钩子允许我们读取和写入Context对象中的数据。
js
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传递给不需要这些值的多层组件,这让重构工作变得容易得多。
Provider模式非常适合共享全局数据。一个常见的应用场景是与多个组件共享主题UI状态。
假设我们有一个展示列表的简单应用。我们希望用户能够通过切换开关在亮色模式和暗色模式之间切换。当用户在两种模式间切换时,背景色和文字色应该随之改变!我们无需将当前主题值传递给每个组件,而是可以用
ThemeProvider
包裹组件,并将当前主题颜色传递给Provider。
js
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
函数来相应地更新主题。
js
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。
js
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
useContext
and the Context in each component, we can use a hook that returns the context we need.
js
function useThemeContext() {
  const theme = useContext(ThemeContext);
  return theme;
}
To make sure that it's a valid theme, let's throw an error if
useContext(ThemeContext)
returns a falsy value.
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
ThemeContext.Provider
component, we can extract a dedicated provider component. This keeps the context logic separate from the rendering components and improves reusability.
js
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
ThemeContext
, can now simply use the
useThemeContext
hook.
js
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。无需在每个组件中都导入
useContext
和Context,我们可以使用一个钩子来返回所需的Context。
js
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组件,而不是直接用
ThemeContext.Provider
组件包裹其他组件。这可以将Context逻辑与渲染组件分离,提高复用性。
js
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
钩子。
js
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
ThemeProvider
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!
js
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
style
prop to the
ListItem
component, we'll make it a
styled.li
component. Since it's a styled component, we can access the value of
theme
!
js
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库为我们提供了一个
ThemeProvider
。每个styled组件都可以访问这个Provider的值!我们无需自己创建Context API,直接使用库提供的即可!
js
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组件,我们可以访问
theme
的值!
js
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组件应用样式!

Tradeoffs

权衡

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
Increment
button in the
Button
component. We also have a
Reset
button in the
Reset
component, which resets the count back to
0
.
The
Reset
component also re-rendered since it consumed the
useCountContext
. 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.
To 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的组件都会在状态变化时重新渲染。
让我们看一个例子。我们有一个简单的计数器,每次点击
Button
组件中的
Increment
按钮,计数器的值就会增加。我们还有一个
Reset
组件中的
Reset
按钮,可以将计数重置为
0
由于
Reset
组件也消费了
useCountContext
,它也会重新渲染。在小型应用中,这不会有太大影响。但在大型应用中,将频繁更新的值传递给多个组件可能会对性能产生负面影响。
为了避免组件消费包含不必要的、可能会更新的值的Provider,你可以为每个不同的使用场景创建多个Provider。

Source

来源

References

参考资料