• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • React 通关秘籍

    • 1.关于本小册
    • 2.一网打尽组件常用Hook
    • 3.Hook的闭包陷阱的成因和解决方案
    • 4.React组件如何写TypeScript类型
    • 5.React组件如何调试
    • 6.受控模式VS非受控模式
    • 7.组件实战:迷你Calendar
    • 8.组件实战:Calendar日历组件(上)
    • 9.组件实战:Calendar日历组件(下)
    • 10.快速掌握Storybook
    • 11.React组件如何写单测
    • 12.深入理解Suspense和ErrorBoundary
    • 13.组件实战:Icon图标组件
    • 14.组件实战:Space间距组件
    • 15.React.Children和它的两种替代方案
    • 16.三个简单组件的封装
    • 17.浏览器的5种Observer
    • 18.组件实战:Watermark防删除水印组件
    • 19.手写react-lazyload
    • 20.图解网页的各种距离
    • 21.自定义hook练习
    • 22.自定义hook练习(二)
    • 23.用react-spring做弹簧动画
    • 24.react-spring结合use-gesture手势库实现交互动画
    • 25.用react-transition-group和react-spring做过渡动画
    • 26.快速掌握Tailwind:最流行的原子化CSS框架
    • 27.用CSSModules避免样式冲突
    • 28.CSSInJS:快速掌握styled-components
    • 29.react-spring实现滑入滑出的转场动画
    • 30.组件实战:Message全局提示组件
    • 31.组件实战:Popover气泡卡片组件
    • 32.项目里如何快速定位组件源码
    • 33.一次超爽的React调试体验
    • 34.组件实战:ColorPicker颜色选择器(一)
    • 35.组件实战:ColorPicker颜色选择器(二)
    • 36.组件实战:onBoarding漫游式引导组件
    • 37.组件实战:Upload拖拽上传
    • 38.组件实战:Form表单组件
    • 39.React组件库都是怎么构建的
    • 40.组件库实战:构建esm和cjs产物,发布到npm
    • 41.组件库实战:构建umd产物,通过unpkg访问
    • 42.数据不可变:immutable和immer
    • 43.基于ReactRouter实现keepalive
    • 44.Historyapi和ReactRouter实现原理
    • 45.ReactContext的实现原理和在antd里的应用
    • 46.ReactContext的性能缺点和解决方案
    • 47.手写一个Zustand
    • 48.原子化状态管理库Jotai
    • 49.用react-intl实现国际化
    • 50.国际化资源包如何通过Excel和GoogleSheet分享给产品经理
    • 51.基于react-dnd实现拖拽排序
    • 52.react-dnd实战:拖拽版TodoList
    • 53.ReactPlayground项目实战:需求分析、实现原理
    • 54.ReactPlayground项目实战:布局、代码编辑器
    • 55.ReactPlayground项目实战:多文件切换
    • 56.ReactPlayground项目实战:babel编译、iframe预览
    • 57.ReactPlayground项目实战:文件增删改
    • 58.ReactPlayground项目实战:错误显示、主题切换
    • 59.ReactPlayground项目实战:链接分享、代码下载
    • 60.ReactPlayground项目实战:WebWorker性能优化
    • 61.ReactPlayground项目实战:总结
    • 62.手写MiniReact:思路分析
    • 63.手写MiniReact:代码实现
    • 64.手写MiniReact:和真实React源码的对比
    • 65.React18的并发机制是怎么实现的
    • 66.Ref的实现原理
    • 67.低代码编辑器:核心数据结构、全局store
    • 68.低代码编辑器:拖拽组件到画布、拖拽编辑json
    • 69.低代码编辑器:画布区hover展示高亮框
    • 70.低代码编辑器:画布区click展示编辑框
    • 71.低代码编辑器:组件属性、样式编辑
    • 72.低代码编辑器:预览、大纲
    • 73.低代码编辑器:事件绑定
    • 74.低代码编辑器:动作弹窗
    • 75.低代码编辑器:自定义JS
    • 76.低代码编辑器:组件联动
    • 77.低代码编辑器:拖拽优化、Table组件
    • 78.低代码编辑器:Form组件、store持久化
    • 79.低代码编辑器:项目总结
    • 80.快速掌握ReactFlow画流程图
    • 81.ReactFlow振荡器调音:项目介绍
    • 82.ReactFlow振荡器调音:流程图绘制
    • 83.ReactFlow振荡器调音:合成声音
    • 84.AudioContext实现在线钢琴
    • 85.React服务端渲染:从SSR到hydrate
    • 86.小册总结

上节我们学了 Context 的用法、实现原理,还有它在 antd 里的应用。

在跨层传递数据方面,确实很好用,在组件库里有很多应用。

但是它也有一些缺点。

我们创建个项目来试下:

npx create-vite

改下 App.tsx

import {
    FC,
    PropsWithChildren,
    createContext,
    useContext,
    useState,
} from "react";

interface ContextType {
    aaa: number;
    bbb: number;
    setAaa: (aaa: number) => void;
    setBbb: (bbb: number) => void;
}

const context = createContext<ContextType>({
    aaa: 0,
    bbb: 0,
    setAaa: () => {},
    setBbb: () => {},
});

const Provider: FC<PropsWithChildren> = ({ children }) => {
    const [aaa, setAaa] = useState(0);
    const [bbb, setBbb] = useState(0);

    return (
        <context.Provider
            value={{
                aaa,
                bbb,
                setAaa,
                setBbb,
            }}>
            {children}
        </context.Provider>
    );
};

const App = () => (
    <Provider>
        <Aaa />
        <Bbb />
    </Provider>
);

const Aaa = () => {
    const { aaa, setAaa } = useContext(context);

    console.log("Aaa render...");

    return (
        <div>
            aaa: {aaa}
            <button onClick={() => setAaa(aaa + 1)}>加一</button>
        </div>
    );
};

const Bbb = () => {
    const { bbb, setBbb } = useContext(context);

    console.log("Bbb render...");

    return (
        <div>
            bbb: {bbb}
            <button onClick={() => setBbb(bbb + 1)}>加一</button>
        </div>
    );
};

export default App;

用 createContext 创建了 context,其中保存了 2 个useState 的 state 和 setState 方法。

用 Provider 向其中设置值,在 Aaa、Bbb 组件里用 useContext 取出来渲染。

去掉 main.tsx 里的 index.css 和 StrictMode:

把服务跑起来:

npm install
npm run dev

浏览器访问下:

可以看到,修改 aaa 的时候,会同时触发 bbb 组件的渲染,修改 bbb 的时候,也会触发 aaa 组件的渲染。

因为不管修改 aaa 还是 bbb,都是修改 context 的值,会导致所有用到这个 context 的组件重新渲染。

这就是 Context 的问题。

解决方案也很容易想到:拆分成两个 context 不就不会互相影响了?

import {
    FC,
    PropsWithChildren,
    createContext,
    useContext,
    useState,
} from "react";

interface AaaContextType {
    aaa: number;
    setAaa: (aaa: number) => void;
}

const aaaContext = createContext<AaaContextType>({
    aaa: 0,
    setAaa: () => {},
});

interface BbbContextType {
    bbb: number;
    setBbb: (bbb: number) => void;
}

const bbbContext = createContext<BbbContextType>({
    bbb: 0,
    setBbb: () => {},
});

const AaaProvider: FC<PropsWithChildren> = ({ children }) => {
    const [aaa, setAaa] = useState(0);

    return (
        <aaaContext.Provider
            value={{
                aaa,
                setAaa,
            }}>
            {children}
        </aaaContext.Provider>
    );
};

const BbbProvider: FC<PropsWithChildren> = ({ children }) => {
    const [bbb, setBbb] = useState(0);

    return (
        <bbbContext.Provider
            value={{
                bbb,
                setBbb,
            }}>
            {children}
        </bbbContext.Provider>
    );
};

const App = () => (
    <AaaProvider>
        <BbbProvider>
            <Aaa />
            <Bbb />
        </BbbProvider>
    </AaaProvider>
);

const Aaa = () => {
    const { aaa, setAaa } = useContext(aaaContext);

    console.log("Aaa render...");

    return (
        <div>
            aaa: {aaa}
            <button onClick={() => setAaa(aaa + 1)}>加一</button>
        </div>
    );
};

const Bbb = () => {
    const { bbb, setBbb } = useContext(bbbContext);

    console.log("Bbb render...");

    return (
        <div>
            bbb: {bbb}
            <button onClick={() => setBbb(bbb + 1)}>加一</button>
        </div>
    );
};

export default App;

这样就好了。

在 antd 里,也是不同的数据放到不同的 context 里:

但这样也会导致 Provider 嵌套过深:

<context1.Provider value={}>
    <context2.Provider value={}>
        <context3.Provider value={}>
            <context4.Provider value={}>
                <context5.Provider value={}>{children}</context5.Provider>
            </context4.Provider>
        </context3.Provider>
    </context2.Provider>
</context1.Provider>

所以 context 来存放一些配置数据还好,比如 theme、size 等,用来存很多业务数据就不大合适了。

这时候可以用 redux、zustand、jotai 等状态管理库。

它们都不是基于 context 实现的,那自然也没有 context 这种问题。

比如用 zustand:

npm install --save zustand
import { create } from "zustand";

type State = {
    aaa: number;
    bbb: number;
};

type Action = {
    setAaa: (aaa: State["aaa"]) => void;
    setBbb: (bbb: State["bbb"]) => void;
};

const useStore = create<State & Action>((set) => ({
    aaa: 0,
    bbb: 0,
    setAaa: (aaa) => set(() => ({ aaa })),
    setBbb: (bbb) => set(() => ({ bbb })),
}));

const App = () => (
    <div>
        <Aaa />
        <Bbb />
    </div>
);

const Aaa = () => {
    const aaa = useStore((state) => state.aaa);
    const setAaa = useStore((state) => state.setAaa);

    console.log("Aaa render...");

    return (
        <div>
            aaa: {aaa}
            <button onClick={() => setAaa(aaa + 1)}>加一</button>
        </div>
    );
};

const Bbb = () => {
    const bbb = useStore((state) => state.bbb);
    const setBbb = useStore((state) => state.setBbb);

    console.log("Bbb render...");

    return (
        <div>
            bbb: {bbb}
            <button onClick={() => setBbb(bbb + 1)}>加一</button>
        </div>
    );
};

export default App;

zustand 虽然也是集中存放的数据,但是内部做了处理,更新某个 state 不会导致依赖其它 state 的组件重新渲染:

此外,不用状态管理库,不拆分 context,也可以解决,比如用 memo:

memo 会对新旧 props 做对比,只有 props 变化了才会渲染。

这样就能避免没必要的渲染。

import {
    FC,
    PropsWithChildren,
    createContext,
    memo,
    useCallback,
    useContext,
    useState,
} from "react";

interface CounterContext {
    aaa: number;
    bbb: number;
    setAaa: (aaa: number) => void;
    setBbb: (bbb: number) => void;
}

const context = createContext<CounterContext>({
    aaa: 0,
    bbb: 0,
    setAaa: () => {},
    setBbb: () => {},
});

const Provider: FC<PropsWithChildren> = ({ children }) => {
    const [aaa, setAaa] = useState(0);
    const [bbb, setBbb] = useState(0);

    return (
        <context.Provider
            value={{
                aaa,
                bbb,
                setAaa,
                setBbb,
            }}>
            {children}
        </context.Provider>
    );
};

const App = () => (
    <Provider>
        <WrappedAaa />
        <WrappedBbb />
    </Provider>
);

const WrappedAaa = () => {
    const { aaa, setAaa } = useContext(context);

    return <Aaa aaa={aaa} setAaa={setAaa} />;
};

interface AaaProps {
    aaa: number;
    setAaa: (aaa: number) => void;
}

const Aaa = memo((props: AaaProps) => {
    const { aaa, setAaa } = props;

    console.log("Aaa render...");

    return (
        <div>
            aaa: {aaa}
            <button onClick={() => setAaa(aaa + 1)}>加一</button>
        </div>
    );
});

const WrappedBbb = () => {
    const { bbb, setBbb } = useContext(context);

    return <Bbb bbb={bbb} setBbb={setBbb} />;
};

interface BbbProps {
    bbb: number;
    setBbb: (bbb: number) => void;
}

const Bbb = memo((props: BbbProps) => {
    const { bbb, setBbb } = props;

    console.log("Bbb render...");

    return (
        <div>
            bbb: {bbb}
            <button onClick={() => setBbb(bbb + 1)}>加一</button>
        </div>
    );
});

export default App;

这样也能解决 context 会导致额外渲染的问题。

案例代码上传了小册仓库

总结

context 在跨层传递数据方面很好用,在组件库里用的很多,但是它也有一些性能方面的缺点。

context 中如果是一个对象,不管任意属性变了,都会导致依赖其它属性的组件跟着重新渲染。

解决这个问题有几种方案:

  • 拆分 context,每种数据放在一个 context 里
  • 用 zustand 等状态管理库,因为它们不是用 context 实现的,自然没有这种问题
  • 用 memo 包裹子组件,它会对比新旧 props,没变就不会重新渲染

context 虽然好用,但是用的时候也要注意下这个性能方面的缺点。

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
45.ReactContext的实现原理和在antd里的应用
Next
47.手写一个Zustand