用typescript写React Context案例详细讲解

3,417 阅读4分钟

本文已参加「新人创作礼」活动,一起开启掘金创作之路。

开篇

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;

代码讲解:

  1. 在创建Context要写初始值,但执行createContext时更新初始值的函数还没有拿到,所以建议类型加个null初始值写null。
  2. Provider内部可能有多个子组件,所以它的children类型应该定义为React.ReactNode[]。
  3. 从上述函数组件和类组件代码对比可以看出,实现同样功能使用函数组件比用类组件代码少很多行。

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>
    );
}

代码讲解:

  1. 函数组件使用useContext拿数据,直接可以获得数据类型,注意useContext函数后面的感叹号,如果不加typescript会报数据可能为null。
  2. 类组件使用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非常简单,用法看上面的代码,没啥特别的。

结束语

以上代码案例函数组件类组件性能优化双层Context完整代码请点链接访问codesanbox查看