# 提供者模式

TIP

使数据可用于多个子组件

# 例子

在某些情况下,我们希望为应用程序中的许多(如果不是全部)组件提供可用数据。虽然我们可以使用 props 将数据传递给组件,但是如果应用程序中的几乎所有组件都需要访问 props 的值,那么这可能很难做到。

我们经常以一种叫做属性下钻的东西来实现,这种情况下,我们传递属性到整个组件树。重构依赖于 props 的代码几乎是不可能的,而且很难知道某些数据来自哪里。

假设我们有一个包含特定数据的 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>

用这种方式传递道具会很麻烦。如果我们想在将来重命名 data 属性,我们必须在所有组件中重命名它。应用程序的规模越大,属性下钻的难度就越大。

如果我们可以跳过所有不需要使用此数据的组件层,那将是最佳的。我们需要一些东西,让需要访问 data 属性的组件直接访问它,而不依赖于属性下钻。

这就是提供者模式可以帮助我们的地方!使用提供程序模式,我们可以使数据对多个组件可用。我们可以将所有组件封装在一个 Provider 中,而不是通过道具将数据传递到每一层。提供者是 Context 对象提供给我们的高阶组件。我们可以使用 React 为我们提供的 createContext 方法创建 Context 对象。

提供者接收一个 value 属性,其中包含我们要传递的数据。包装在此提供程序中的所有组件都可以访问 value 属性的值。

 






 


 




const DataContext = React.createContext()
 
function App() {
  const data = { ... }
 
  return (
    <div>
      <DataContext.Provider value={data}>
        <SideBar />
        <Content />
      </DataContext.Provider>
    </div>
  )
}

我们不再需要手动向每个组件传递数据支持!那么,ListItem、 Header 和 Text 组件如何访问数据的值呢?

通过使用 useContext 挂钩,每个组件都可以访问数据。此钩子接收数据具有引用的上下文,在本例中为 DataContext。UseContext 钩子允许我们向上下文对象读写数据。






 




 




 



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 属性的组件将道具传递到多个级别,这使得重构变得容易得多。


Provider 模式对于共享全局数据非常有用。提供者模式的一个常见用例是与许多组件共享一个主题 UI 状态。

假设我们有一个显示列表的简单应用程序。

我们希望用户能够切换之间的光模式和暗模式,通过切换开关。当用户从暗光模式切换到明光模式,反之亦然时,背景颜色和文本颜色应该改变!我们可以将组件包装在 ThemeProvider 中,并将当前主题颜色传递给提供程序,而不是将当前主题值传递给每个组件。

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 提供程序中,因此我们可以访问作为值传递给提供程序的 value 主题和 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 中使用主题上下文。

 



 




import React, { useContext } from "react";
import { ThemeContext } from "./App";
 
export default function TextBox() {
  const theme = useContext(ThemeContext);
 
  return <li style={theme.theme}>...</li>;
}

完美!我们不必将任何数据传递给不关心主题当前值的组件。

# Hooks

我们可以创建一个钩子来为组件提供上下文。不必在每个组件中导入 useContext 和 Context,我们可以使用一个钩子来返回我们需要的上下文。

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 组件包装组件不同,我们可以创建一个 HOC 来包装组件以提供其值。这样,我们可以将上下文逻辑从呈现组件中分离出来,从而提高提供程序的可重用性。

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 (
    <div className={`App theme-${theme}`}>
      <ThemeProvider>
        <Toggle />
        <List />
      </ThemeProvider>
    </div>
  );
}

需要访问 ThemeContext 的每个组件现在都可以简单地使用 useThemeContext 挂钩。


 




export default function TextBox() {
  const theme = useThemeContext();
 
  return <li style={theme.theme}>...</li>;
}

通过为不同的上下文创建钩子,可以很容易地将提供者的逻辑与呈现数据的组件分离开来。

# 取舍

# 优点

提供者模式/上下文 API 使得将数据传递给许多组件成为可能,而不必手动通过每个组件层传递数据。

它降低了在重构代码时意外引入 bug 的风险。以前,如果我们稍后想要重命名属性,我们必须在使用该值的整个应用程序中重命名属性。

我们不再需要处理属性下钻,这可以被看作是一种反模式。以前,可能很难理解应用程序的数据流,因为并不总是清楚某些属性值来自哪里。使用 Provider 模式,我们不再需要将道具不必要地传递给不关心此数据的组件。

使用 Provider 模式可以很容易地保持某种全局状态,因为我们可以让组件访问这种全局状态。

# 缺点

在某些情况下,过度使用 Provider 模式会导致性能问题。在每个状态更改时使用上下文重新呈现的所有组件。

让我们看一个例子。我们有一个简单的计数器,每次单击 Button 组件中的 Increment 按钮时,该计数器的值都会增加。我们在 Reset 组件中还有一个 Reset 按钮,它将计数重置回0。

然而,当你点击“增量”时,你可以看到重新呈现的不仅仅是计数。Reset 组件中的日期也会重新呈现!

import React, { useState, createContext, useContext, useEffect } from "react";
import ReactDOM from "react-dom";
import moment from "moment";

import "./styles.css";

const CountContext = createContext(null);

function Reset() {
  const { setCount } = useCountContext();

  return (
    <div className="app-col">
      <button onClick={() => setCount(0)}>Reset count</button>
      <div>Last reset: {moment().format("h:mm:ss a")}</div>
    </div>
  );
}

function Button() {
  const { count, setCount } = useCountContext();

  return (
    <div className="app-col">
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <div>Current count: {count}</div>
    </div>
  );
}

function useCountContext() {
  const context = useContext(CountContext);
  if (!context)
    throw new Error(
      "useCountContext has to be used within CountContextProvider"
    );
  return context;
}

function CountContextProvider({ children }) {
  const [count, setCount] = useState(0);
  return (
    <CountContext.Provider value={{ count, setCount }}>
      {children}
    </CountContext.Provider>
  );
}

function App() {
  return (
    <div className="App">
      <CountContextProvider>
        <Button />
        <Reset />
      </CountContextProvider>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));

Reset 组件也重新呈现,因为它使用了 useCountContext。在较小的应用程序中,这不会太重要。在较大的应用程序中,将频繁更新的值传递给许多组件可能会对性能产生负面影响。

为了确保组件不会使用包含可能更新的不必要值的提供程序,您可以为每个单独的用例创建几个提供程序。