# 提供者模式
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。在较小的应用程序中,这不会太重要。在较大的应用程序中,将频繁更新的值传递给许多组件可能会对性能产生负面影响。
为了确保组件不会使用包含可能更新的不必要值的提供程序,您可以为每个单独的用例创建几个提供程序。