在某些情况下,我们希望为应用程序中的许多(如果不是全部)组件提供可用数据。尽管可以使用 props
将数据传递给组件,但如果应用程序中几乎所有组件都需要访问 props 的值,那么这可能很难做到。
我们经常会遇到一种称为“ prop drilling”的情况,将props传递到组件树的深处时,就会出现这种情况。重构依赖于 props 的代码几乎是不可能的,并且知道某些数据来自哪里也很困难。
假设有一个包含某些 data 的 App
组件。在组件树的最深处,有一个 ListItem
、 Header
和 Text
组件,它们都需要来自 App
的data。为了将这些数据传递给这些组件,必须将其传递给多层组件。
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 钻取就越棘手。
如果我们能够跳过不需要使用这些数据的所有组件层,那将是最佳的。我们需要有一些东西可以让需要访问 data
值的组件直接访问它,而不依赖于 prop 钻孔。
通过提供者模式,我们可以使得 data 供多个组件使用,可以将所有组件包装在 Provider
中,而不是通过 props 将数据传递到每一层。 Provider 是由 Context
对象提供给高阶组件。可以使用 React 提供的 createContext
方法创建一个 Context 对象。
Provider 接收一个 value
属性,其中包含我们想要传递的数据。包含在该提供程序中的所有组件都可以访问 value
属性的值。
const DataContext = React.createContext()
function App() {
const data = { ... }
return (
<div>
<DataContext.Provider value={data}>
<SideBar />
<Content />
</DataContext.Provider>
</div>
)
}
我们不再需要手动将 data
属性传递给每个组件。那么, ListItem
、 Header
和 Text
组件如何访问 data
的值呢?
每个组件都可以通过使用 useContext
挂钩来访问 data
。此挂钩接收 data
具有引用的上下文,在本例中为 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>;
}
不使用 data
值的组件根本不必处理 data
。我们不再需要担心通过不需要 props 值的组件将 props 向下传递几个级别,这使得重构变得更加容易。
提供者模式对于共享全局数据非常有用。提供者模式的一个常见用例是与许多组件共享主题 UI 状态。
假设我们有一个显示列表的简单应用程序。
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>
);
}
我们希望用户能够通过切换开关在浅色模式和深色模式之间切换。当用户从浅色模式切换到深色模式时,反之亦然,背景颜色和文本颜色应该改变。我们可以将组件包装在 ThemeProvider
中,并将当前主题颜色传递给提供程序,而不是将当前主题值传递给每个组件。
由于 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
中使用 theme
上下文。不必将任何数据传递给不关心主题当前值的组件。
import React, { useContext } from "react";
import { ThemeContext } from "./App";
export default function TextBox() {
const theme = useContext(ThemeContext);
return <li style={theme.theme}>...</li>;
}
Hooks
我们可以创建一个 hook 来为组件提供上下文。使用一个返回需要的上下文的钩子,而无须在每个组件中导入 useContext
和上下文。
为了确保它是一个有效的主题,如果 useContext(ThemeContext)
返回一个虚假值,我们将抛出一个错误。
function useThemeContext() {
const theme = useContext(ThemeContext);
if (!theme) {
throw new Error("useThemeContext must be used within ThemeProvider");
}
return theme;
}
我们可以创建一个 HOC 来包装组件以提供其值,而不是直接使用 ThemeContext.Provider
组件来包装组件。这样,我们可以将上下文逻辑与渲染组件分离,从而提高提供者的可重用性。
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>;
}
通过为不同的上下文创建钩子,可以轻松地将提供者的逻辑与呈现数据的组件分开。
案例分析
一些库提供内置提供程序,我们可以在使用组件中使用这些值。一个很好的例子是styled-components。
No experience with styled-components is needed to understand this example.
styled-components库为我们提供了一个 ThemeProvider
。每个样式组件都可以访问该提供程序的值。我们可以使用提供给我们的 API,而不是自己创建上下文 API。
让我们使用相同的 List 示例,并将组件包装在从 styled-component
库导入的 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>
);
}
我们不会将内联 style
属性传递给 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};
`}
`;
优点
Provider 模式/Context API 使得将数据传递到许多组件成为可能,而无需手动通过每个组件层传递数据。
它降低了重构代码时意外引入错误的风险。以前,如果我们稍后想要重命名某个 prop,则必须在使用该值的整个应用程序中重命名该 prop。
我们不再需要处理 prop-drilling,,这可以被视为一种反模式。以前,理解应用程序的数据流可能很困难,因为并不总是清楚某些 prop 值的起源。使用 Provider 模式,我们不再需要将 props 不必要地传递给不关心这些数据的组件。
使用提供者模式可以轻松保持某种全局状态,因为我们可以让组件访问此全局状态。
缺点
在某些情况下,过度使用提供者模式可能会导致性能问题。所有使用上下文的组件都会在每次状态更改时重新渲染。
举个例子,有一个简单的计数器,每次单击 Button
组件中的 Increment
按钮时,该计数器的值就会增加。在 Reset
组件中还有一个 Reset
按钮,它将计数重置回 0
。
但是,当单击 Increment
时,会发现重新渲染的不仅仅是计数。 Reset
组件中的日期也会重新呈现。
Reset
组件也重新渲染,因为它消耗了 useCountContext
。在较小的应用程序中,这并不重要。在较大的应用程序中,将频繁更新的值传递给许多组件可能会对性能产生负面影响。
为了确保组件不会使用包含可能更新的不必要值的 Provider,可以为特定的用例创建多个 Provider。