提供者模式
在某些情况下,我们希望为应用中的许多(如果不是全部)组件提供可用数据。虽然我们可以使用 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
中,而不是通过属性将数据向下传递到每一层。Provider 是 Context
对象提供给我们的高阶组件。我们可以使用 React 为我们提供的 createContext
方法创建一个 Context 对象。
Provider 接收一个 value
属性,其中包含我们要传递的数据。包裹在此 provider 中的所有组件都可以访问 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
。我们不再需要担心通过不需要属性值的组件将属性向下传递几层,这使得重构变得更加容易。
提供者模式对于共享全局数据非常有用。提供者模式的一个常见用例是与许多组件共享主题 UI 状态。
假设我们有一个显示列表的简单应用。
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
const rootElement = document.getElementById("root");
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
rootElement
);
我们希望用户能够通过切换开关在亮模式和暗模式之间切换。当用户从暗模式切换到亮模式时,反之亦然,背景颜色和文本颜色应该改变!而不是将当前主题值传递给每个组件,我们可以将组件包装在 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
传递给提供者的值 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>;
}
完美!我们不必将任何数据传递给不关心主题当前值的组件。
import React, { useState } from "react";
import "./styles.css";
import List from "./List";
import Toggle from "./Toggle";
export const themes = {
light: {
background: "#fff",
color: "#000"
},
dark: {
background: "#171717",
color: "#fff"
}
};
export const ThemeContext = React.createContext();
export default function App() {
const [theme, setTheme] = useState("dark");
function toggleTheme() {
setTheme(theme === "light" ? "dark" : "light");
}
return (
<div className={`App theme-${theme}`}>
<ThemeContext.Provider value={{ theme: themes[theme], toggleTheme }}>
<>
<Toggle />
<List />
</>
</ThemeContext.Provider>
</div>
);
}
钩子
我们可以创建一个钩子来为组件提供上下文。不必在每个组件中导入 useContext
和上下文,我们可以使用一个钩子来返回我们需要的上下文。
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
组件包装组件。这样,我们可以将上下文逻辑与渲染组件分离,从而提高提供者的可重用性。
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>;
}
通过为不同的上下文创建钩子,很容易将提供者的逻辑与呈现数据的组件分开。
案例分析
一些库提供内置提供程序,我们可以在消费组件中使用这些值。一个很好的例子就是 样式化组件。
“理解这个例子不需要任何风格组件的经验。”
样式化组件库为我们提供了 ThemeProvider
。每个样式化的组件都可以访问这个提供者的值!我们可以使用提供给我们的上下文接口,而不是自己创建上下文接口!
让我们使用相同的 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};
`}
`;
太棒了,我们现在可以使用 ThemeProvider
轻松地将样式应用于我们所有样式化的组件!
import React, { useState } from "react";
import { ThemeProvider } from "styled-components";
import "./styles.css";
import List from "./List";
import Toggle from "./Toggle";
export 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");
}
return (
<div className={`App theme-${theme}`}>
<ThemeProvider theme={themes[theme]}>
<>
<Toggle toggleTheme={toggleTheme} />
<List />
</>
</ThemeProvider>
</div>
);
}
优点
提供者模式/上下文 API 使得将数据传递给许多组件成为可能,而无需手动通过每个组件层传递数据。
它降低了重构代码时意外引入错误的风险。以前,如果我们以后想要重命名一个属性,我们必须在整个使用这个值的应用中重命名这个属性。
我们不再需要处理属性穿透,这可以被视为一种反模式。以前,可能很难理解应用的数据流,因为某些属性值的来源并不总是很清楚。使用提供者模式,我们不再需要将属性不必要地传递给不关心这些数据的组件。
使用提供者模式可以很容易地保持某种全局状态,因为我们可以让组件访问这种全局状态。
缺点
在某些情况下,过度使用提供者模式会导致性能问题。所有使用上下文的组件都会在每次状态更改时重新渲染。
让我们看一个例子。我们有一个简单的计数器,每次单击 Button
组件中的 Increment
按钮时,其值都会增加。我们在 Reset
组件中还有一个 reset
按钮,它将计数重置回 0
。
但是,当您单击 Increment
时,您会看到重新渲染的不仅仅是计数。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
。在较小的应用中,这无关紧要。在较大的应用中,将频繁更新的值传递给许多组件可能会对性能产生负面影响。
为了确保组件不使用包含可能更新的不必要值的提供者,您可以为每个单独的用例创建多个提供者。