在一些场景,我们希望让一些数据在应用中的许多组件中可用,但不是全部组件。尽管我们可以通过使用 Props 在组件之间传递数据,但是如果几乎全部的组件在你的应用中都需要访问这个数据实现起来会比较难。
我们通过 Props 传递数据到组件树很远的地方,这种方式我们通常叫做 Prop Drilling;重构依赖 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>
通过 Props 传递数据这种方式将变得非常混乱,如果我们将来想要对数据进行重新命名,我们必须要在所有组件中修改它。你的应用越大,Prop Drilling 问题就越棘手。
如果我们可以跳过所有不需要使用这些数据的组件层将会是最佳选择。我们需要一个东西来给予组件直接访问数据的能力,而不是依赖 Prop Drilling。
这就是提供者模式可以帮助我们的地方,通过提供者模式,我们可以让数据在多个组件中生效,而不是通过 Props 向下传递数据。我们可以使用 Provider 组件包括所有其他组件。 Provider 组件是一个通过 Context 对象提供给我们的高阶组件,我们可以使用 React 提供给我们的 createContext 函数来创建 Context 对象。
Provider 组件接收一个我们希望向下传递的数据,所有被 Provider 包裹的组件都可以访问这个数据。
const DataContext = React.createContext()
function App() {
const data = { ... }
return (
<div>
<DataContext.Provider value={data}>
<SideBar />
<Content />
</DataContext.Provider>
</div>
)
}
我们没有手动向下传递数据给没一个组件,所以 ListItem、Header 和 Text 组件是如何访问到数据的?
每一个组件可以通过 useContext 钩子函数来访问数据。这个钩子函数接收一个拥有数据引用的 Context 对象。useContext 钩子函数让我们可以读取和写入数据到 Context 对象。
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 向下传递经过了几层组件,这让重构变得更简单。
提供者模式对于分享全局数据来说非常实用。对于提供者模式通常用来在许多组件中分享主题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
);
我们想要用户可以切换 light 模式和 dark 模式,当用户从 dark 模式切换到 light 模式或者从 light 模式切换到 dark 模式,背景颜色和文字颜色应该发生变化。我们可以使用 ThemeProvider 组件包裹其他组件并将当前的主题颜色传递给 Provider 来代替将当前的主体颜色向下传递给每一个组件。
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 Provider 组件中,我们可以访问传递给 Provider 的 theme 和 toggleTheme。
在 Toggle 组件中,我们可以使用 toggleTheme 函数更新 theme。
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 组件自己不关注 theme 当前的值,可是 ListItem 组件需要,我们可以直接在 ListItem 组件内使用 theme Context。
import React, { useContext } from "react";
import { ThemeContext } from "./App";
export default function TextBox() {
const theme = useContext(ThemeContext);
return <li style={theme.theme}>...</li>;
}
完美!我们不再需要通过不关心 theme 的组件来向下传递它。
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>
);
}
Hooks
我们可以创建一个 Hook 函数来给组件提供 Context,代替通过在每一个组件中引入 useContext 和 Context;我们可以使用 Hook 函数返回我们需要的 Context 实例。
function useThemeContext() {
const theme = useContext(ThemeContext);
return theme;
}
确认 theme 是一个有效的值,如果 useContext(ThemeContext) 返回 false,让我们抛出一个异常。
function useThemeContext() {
const theme = useContext(ThemeContext);
if (!theme) {
throw new Error("useThemeContext must be used within ThemeProvider");
}
return theme;
}
我们可以创建一个高阶组件来代替直接使用 ThemeContext.Provider 包裹其他组件。通过这种方式,我们可以将 Context 代码逻辑与渲染组件分离,从而提高 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 Hook 来达到目的。
export default function TextBox() {
const theme = useThemeContext();
return <li style={theme.theme}>...</li>;
}
通过为不同的 Context 创建不同的 Hooks,可以很简单的分离 Provider 逻辑与数据渲染。
案例分析
一些组件提供了内置的 Provider。我们可以在使用他们的时候使用这些值,一个很好的例子就是 styled-components。
styled-components 库为我们提供了一个 ThemeProvider,每一个 styled 组件都可以访问这个 Provider 提供的数据,我们可以使用 styled-components 提供给我们的 Provider 而不是我们自己使用 Context API 创建。
让我们使用相同的列表例子,通过引入 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>
);
}