本文已参加「新人创作礼」活动,一起开启掘金创作之路。
开篇
React官网上有关Context的介绍摘要如下: Context设计的目的是为了共享那些对组件树而言是“全局”的数据,如:当前认证的用户、主题、语言等。Context提供了一个无需为每层组件手动添加props就能在组件树间进行数据传递的方法。新版本的Context。
但是React官网上的案例有不少问题:一是没有用typescript写,二是没有类组件与函数组件对照的双版本,三是代码不完整。
在此我写一篇完整可以运行的代码案例及讲解。
Context.Provider
首选建议单独写一个文件放Context容器,该文件要导出2个东西,一是数据类型、二是容器组件。
ThemeContext.tsx文件代码:
函数组件:
import React, { createContext, useState } from "react";
// 创建context,约定数据类型,设置初始值
export const ThemeCtx = createContext<{
color: string,
setColor: (color:string)=>void,
} | null>(null)
// ContextProvide组件
const ThemeProvide: React.FC<{
children: React.ReactNode[]
}> = (props) => {
const [color, setColor] = useState("red")
return (
<ThemeCtx.Provider value={{ color, setColor }}>
{props.children}
</ThemeCtx.Provider>
);
};
export default ThemeProvide
类组件:
import React, { createContext } from "react";
// 创建context,约定数据类型,设置初始值
export const ThemeCtx = createContext<{
color: string,
setColor: (color:string)=>void,
} | null>(null);
type Props = {
children: React.ReactNode[]
}
type State = {
color: string,
setColor: (color:string)=>void,
}
class ThemeProvide extends React.Component<Props,State>{
constructor(props: Props) {
super(props);
this.state = { color: "blue", setColor: (color: string) => this.setState({...this.state,color}) };
}
render() {
const { color, setColor } = this.state
return (
<ThemeCtx.Provider value={{ color, setColor }}>
{this.props.children}
</ThemeCtx.Provider>
);
}
}
export default ThemeProvide;
代码讲解:
- 在创建Context要写初始值,但执行createContext时更新初始值的函数还没有拿到,所以建议类型加个null初始值写null。
- Provider内部可能有多个子组件,所以它的children类型应该定义为React.ReactNode[]。
- 从上述函数组件和类组件代码对比可以看出,实现同样功能使用函数组件比用类组件代码少很多行。
Context.Consumer
真心不建议使用Context.Consumer包一层的方式拿数据,太不灵活了!建议拿数据的方式如下,注意类组件和函数组件拿数据的方式有不同。
App.tsx文件代码:
import React, { useContext } from "react"
import ThemeContext, { ThemeCtx } from "./类式组件/ThemeContext"
class Show extends React.Component {
static contextType = ThemeCtx;
context!: React.ContextType<typeof ThemeCtx>
render() {
return (
<div style={{ color: this.context?.color }}>展示内容,字体颜色可切换!</div>
)
}
}
const Button: React.FC = () => {
const { color, setColor } = useContext(ThemeCtx)!
return (
<button
onClick={() => (color === "red" ? setColor("blue") : setColor("red"))}
>
点我切换颜色
</button>
)
}
export default function App() {
return (
<ThemeContext>
<Show />
<Button />
</ThemeContext>
);
}
代码讲解:
- 函数组件使用useContext拿数据,直接可以获得数据类型,注意useContext函数后面的感叹号,如果不加typescript会报数据可能为null。
- 类组件使用
static contextType = ThemeCtx
绑定context即可通过this.context拿到数据。注意:这样拿到的数据类型是unknown,在后面使用时非常不方便,所以必须加1行代码context!: React.ContextType<typeof ThemeCtx>
,如果写的是jsx那么这行代码不用加。
性能优化
代码示例
import { useState, useContext, createContext, useMemo } from "react";
const ThemeCtx = createContext<{
theme: string;
setTheme: React.Dispatch<React.SetStateAction<string>>;
} | null>(null);
const ThemeProvide: React.FC<{
children: React.ReactNode[];
}> = (props) => {
const [theme, setTheme] = useState("dark");
return (
<ThemeCtx.Provider value={{ theme, setTheme }}>
{props.children}
</ThemeCtx.Provider>
);
};
const Theme = () => {
const ctx = useContext(ThemeCtx);
const { theme } = ctx!;
return <div>theme: {theme}</div>;
};
const ChangeButton = () => {
const ctx = useContext(ThemeCtx);
const { setTheme } = ctx!;
const dom = useMemo(() => {
console.log("ChangeButton被渲染!!!");
return (
<div>
<button
onClick={() => setTheme((v) => (v === "light" ? "dark" : "light"))}
>
改变theme
</button>
</div>
);
}, [setTheme]);
return dom;
};
const Other = () => {
console.log("Other组件被渲染!!!");
return <div>other组件,没消费Context数据</div>;
};
export default function App() {
return (
<ThemeProvide>
<ChangeButton />
<Theme />
<Other />
</ThemeProvide>
);
}
讲解
react Context是发布订阅机制的数据共享,被Context.Provider包围住的Context.Consumer子节点会在Provider数据变更时统统会重新渲染。需要注意避免重复渲染,对于有些Context.consumer只做更新不消费数据的组件,可以用memo包一下,另外不能直接把Context.Provider写在App里,这样会造成重复渲染,建议将Context.Provider单独写一个组件,App再引入这个组件,这样可以避免重复渲染。
多层Context
代码
import { useState, useContext, createContext, useMemo } from "react";
const ColorCtx = createContext<{
color: string;
setColor: React.Dispatch<React.SetStateAction<string>>;
} | null>(null);
const ColorProvide: React.FC<{
children: React.ReactNode[] | React.ReactNode;
}> = (props) => {
const [color, setColor] = useState("red");
return (
<ColorCtx.Provider value={{ color, setColor }}>
{props.children}
</ColorCtx.Provider>
)
}
const BackgroundCtx = createContext<{
background: string;
setBackground: React.Dispatch<React.SetStateAction<string>>;
} | null>(null);
const BackgroundProvide: React.FC<{
children: React.ReactNode[] | React.ReactNode;
}> = (props) => {
const [background, setBackground] = useState("black");
return (
<BackgroundCtx.Provider value={{ background, setBackground }}>
{props.children}
</BackgroundCtx.Provider>
)
}
const Color = () => {
const ctx1 = useContext(ColorCtx);
const { color } = ctx1!;
const ctx2 = useContext(BackgroundCtx);
const { background } = ctx2!;
return <div style={{ color, background }}>这段文字的字体颜色和背景颜色会变换</div>;
};
const ChangeButton = () => {
const ctx1 = useContext(ColorCtx);
const { setColor } = ctx1!;
const ctx2 = useContext(BackgroundCtx);
const { setBackground } = ctx2!;
const dom = useMemo(() => {
console.log("ChangeButton被渲染!!!");
return (
<div>
<button
onClick={() => setColor(value => value === "red" ? "blue" : "red")}
>
改变字体颜色
</button>
<button
onClick={() => setBackground(value => value === "black" ? "gray" : "black")}
>
改变背景颜色
</button>
</div>
);
}, [setColor, setBackground]);
return dom;
};
const Other = () => {
console.log("Other组件被渲染!!!");
return <div>other组件,没消费Context数据</div>;
};
export default function App() {
return (
<ColorProvide>
<BackgroundProvide>
<br />
<br />
<Color />
<br />
<ChangeButton />
<br />
<Other />
</BackgroundProvide>
</ColorProvide>
);
}
讲解
多层Context非常简单,用法看上面的代码,没啥特别的。