提供者模式

在某些情况下,我们希望为应用程序中的许多(如果不是全部)组件提供可用数据。


单向数据流 props 存在的问题

虽然我们可以使用 props 将数据传递给组件,但如果应用程序中的几乎所有组件都需要访问 props 的值,这可能很难做到。

我们经常会得到一些叫做 prop 钻孔的东西,当我们将 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 prop,我们必须在所有组件中重命名它。您的应用程序越大,道具钻孔就越棘手。

我们最好可以跳过不需要使用这些数据的所有组件层。我们需要一些东西,让需要访问数据值的组件直接访问它,而不依赖于道具钻取。


Provider 提供者

这就是提供者 Provider 模式可以帮助我们的地方!使用提供者模式,我们可以使数据可供多个组件使用。

我们可以将所有组件包装在一个 Provider 中,而不是通过 props 将数据向下传递到每一层。

Provider 是一个由 Context 对象提供给我们的高阶组件。
我们可以使用 React 为我们提供的 createContext 方法创建一个 Context 对象。

Provider 接收一个 value prop,其中包含我们想要传递的数据。包装在此 Provider 中的所有组件都可以访问 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 DataContext = React.createContext();

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

  return (
    <div>
      <SideBar />
      <Content />
    </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 值的组件将 props 向下传递几个级别,这使得重构更容易。


主题 Theme UI

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

import React, { useContext } from "react";
import { ThemeContext } from "./App";

export default function TextBox() {
  const theme = useContext(ThemeContext);

  return <li style={theme.theme}>...</li>;
}

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


Hooks

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

function useThemeContext() {
  const theme = useContext(ThemeContext);
  return theme;
}

为了确保它是一个有效的主题,让我们在 useContext(ThemeContext) 返回一个虚假值时抛出一个错误。

1
2
3
4
5
6
7
8

function useThemeContext() {
  const theme = useContext(ThemeContext);
  if (!theme) {
    throw new Error("useThemeContext 必须在 ThemeProvider 中使用");
  }
  return theme;
}

我们可以创建一个 HOC,使用提供的值添加返回此组件,而不是直接使用 ThemeContext.Provider 组件包装组件。 通过这种方式,我们可以将上下文逻辑与渲染组件分离,从而提高提供程序的可重用性。

function ThemeProvider() {
  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>;
}

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


案例分析

一些库提供了内置的提供者,我们可以在消费组件中使用这些值。 一个很好的例子就是样式组件。

理解这个例子不需要任何样式组件的经验。

styled-components 库为我们提供了一个 ThemeProvider。 每个样式组件都可以访问此提供程序的值! 我们可以使用提供给我们的 API,而不是自己创建上下文 API!

让我们使用相同的 List 示例,并将组件包装在从样式组件库导入的 ThemeProvider 中。

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 组件,而是将其设为 styled.li 组件。 由于它是一个样式组件,我们可以访问 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 轻松地将样式应用于我们所有的样式组件!


优点

提供者模式/上下文 API 可以将数据传递给许多组件,而无需手动将其传递到每个组件层。

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

我们不再需要处理可被视为反模式的螺旋钻。 以前,可能很难理解应用程序的数据流,因为并不总是清楚某些 prop 值的来源。 使用 Provider 模式,我们不再需要不必要地将 props 传递给不关心这些数据的组件。

使用 Provider 模式可以轻松保持某种全局状态,因为我们可以让组件访问此全局状态。


缺点

在某些情况下,过度使用提供者模式会导致性能问题。 使用上下文的所有组件在每次状态更改时都会重新渲染。

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

但是,当您单击 Increment 时,您会看到重新渲染的不仅仅是计数。 重置组件中的日期也会重新呈现!

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

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


知识点

  • React.createContext()
  • Provider
  • useContext