[React 进阶系列] React Context 案例学习:子组件内更新父组件的状态
一直知道 React Context 是 React 内部实现状态管理的方法,也简单的跟着官方的案例敲了一下使用 Context 进行的渲染,不过始终因为 子组件中更新父组件父组件 这一方法的实现太过麻烦,最终还是决定使用 Redux——毕竟 Redux 虽然内部实现依靠的是 Context,但是它已经封装好了。不过最近还是想要深入的学习一下 React,也就包括其状态管理机制,正好学到一个非常干净的写法,也就借此写一篇笔记。
基础的 Context 以及 useContext hook 使用
基础实现的页面如下:
改动的文件有两个:
-
App.js
主要只是清理了一下页面,让渲染的内容简单一些,并且增加了一个子组件:
import { useTheme } from "./context"; import "./styles.css"; const Child = () => { const themeContext = useTheme(); return <div style={ themeContext}> "Theme" </div>; }; export default function App() { return ( <div className="App"> <Child /> </div> ); }
-
context.js
这是主要实现的部分,也对
themeContext
进行了简单的封装,使得其他的组件不需要每次都机械性的使用useContext(ThemeContext)
这样的代码,实现如下:import React, { useContext } from "react"; const theme = { dark: { backgroundColor: "#333", color: "#ccc", padding: "2em", margin: "2em", }, light: { backgroundColor: "#ccc", color: "#333", padding: "2em", margin: "2em", }, }; const ThemeContext = React.createContext(theme.dark); export const useTheme = () => useContext(ThemeContext);
当然,这是一个比较偷懒的做法,没有使用 <ThemeContext.Provider value={state}> ... </ThemeContext.Provider>
的方法传值,而是直接在创建的时候写死。
下一步就会对 Context 进行二次封装,实现 Provier
的部分。
二次封装 Context
这里使用一个 custom hook 去对 Context 进行一下封装,将 theme
的值存在 custom hook 的 state 中,这样就可以在之后使用 setState
的方式更新 Context 中的值。
更新内容如下:
-
App.js
只有在 App 中新增了一个对
ThemeContextProvider
的 wrapper。需要注意的是,因为在 App 中使用的
ThemeContextProvider
包裹住子组件,所以 App 中是无法获得 ThemeContext 的值,只有子组件才 ”继承“ 了 Context 中的内容。export default function App() { return ( <ThemeContextProvider> <div className="App"> <Child /> </div> </ThemeContextProvider> );
-
context.js
import React, { useContext, useState } from "react"; // 因为变量名冲突的关系,这里将其改成了大写……不过常量大写也比较正常 const THEME = { dark: { backgroundColor: "#333", color: "#ccc", padding: "2em", margin: "2em", }, light: { backgroundColor: "#ccc", color: "#333", padding: "2em", margin: "2em", }, }; const ThemeContext = React.createContext(); export const useTheme = () => useContext(ThemeContext); export const ThemeContextProvider = ({ children }) => { const [theme, setTheme] = useState(THEME.dark); return ( <ThemeContext.Provider value={ theme}>{ children}</ThemeContext.Provider> ); };
之后所要做的事情就是将
setTheme
这个方法传到子组件中。鉴于 Context 只能传一个 value,这里所采取的方法是再次新建一个 Context,其 value 是更新 cuttom hook 中的 set 函数。
新增 Update Context
这一步的做法,相当于对 Context 进行一个三次封装,具体变动如下:
// 之前的THEME没有变化
const ThemeContext = React.createContext();
const ThemeUpdateContext = React.createContext();
export const useTheme = () => useContext(ThemeContext);
export const useThemeUpdate = () => useContext(ThemeUpdateContext);
export const ThemeContextProvider = ({
children }) => {
const [theme, setTheme] = useState(THEME.dark);
const updateTheme = ({
theme }) => {
// 这里只是做一个传值的示范
console.log(theme);
setTheme((prevTheme) =>
prevTheme === THEME.dark ? THEME.light : THEME.dark
);
};
return (
<ThemeContext.Provider value={
theme}>
<ThemeUpdateContext.Provider value={
updateTheme}>
{
children}
</ThemeUpdateContext.Provider>
</ThemeContext.Provider>
);
};
使用 useThemeUpdate 去进行 Context 的状态变化
App.js 中的更新如下:
const Child = () => {
const themeContext = useTheme();
const updateTheme = useThemeUpdate();
return (
<div
style={
themeContext}
onClick={
() => updateTheme({
theme: "hello world" })}
>
"Theme"
</div>
);
};
可以看到,更新的逻辑相对而言还是比较清晰的,最终实现效果如下:
这样看来,如果项目结构相对而言比较简单,对 Redux-Saga 的需求不是很大,直接使用 Context hook 也或许是一个不错的选择,毕竟 hooks 省去了 Context.Consumer
使用回调函数取值这样一个繁复的过程。对于 Context 进行三次封装后,可以直接使用 const someVar = useXXX();
的方法取值,以及 const updateSomeVar = useXXXUpdate();
的方法进行更新也不会显得太过繁复。
最后,如果想要省去一层 wrapper,使用这样的方式也可以:
<ThemeContext.Provider value={
{
theme, updateTheme }}>
{
children}
</ThemeContext.Provider value={
{
theme, updateTheme }}>
// when you need to use it
const {
theme, updateTheme } = useTheme();
不过这也是个人倾向,以及在不在意 typo 和自动导入提示的细节问题了,毕竟大部分时候我个人还是觉得,有自动提示/自动导入还是比较方便的。