1. 为什么需要context-mode:props钻透与状态共享的痛点
1.1 props逐层传递的尴尬
我最早接触“context-mode”这个概念,是在一个内部管理系统的前端改造里。那个系统不算大,但页面层级深:路由组件下面挂布局组件,布局组件下面挂侧边栏和内容区,内容区里又有卡片列表,卡片里又有操作按钮。一开始大家写得很开心,直到有一天需要把“当前登录用户的基础信息”从最外层传到底部按钮组件里去,问题就出来了。
一条数据要穿过四五层组件,每一层都要在props里声明、接收、再往下透传。当时项目里有个组件叫UserCard,它自己根本不需要用户信息,但它下面的子组件需要,所以它也得硬着头皮接住这个props再传递下去。这还不算完,后来需求变了,用户信息里要多一个字段,于是从顶层到底层的所有类型定义、默认值、透传逻辑都要同步改一遍。这种状态管理和组件树深度强耦合的模式,改起来牵一发动全身,迟早要出问题。当时我就意识到:单纯靠props钻透来做跨层级数据共享,是这个模式最大的软肋。
1.2 全局变量与全局单例的坑
很多人会说:既然props钻透这么痛苦,那挂到window上不就行了?说实话,我也见过这么干的项目,但这种做法带来的问题比它解决的更多。第一,挂在全局对象上的数据没有“响应性”,组件读取之后数据变了,UI不会跟着刷新,你还得手动在事件里调用一次setState去触发渲染,很容易漏。第二,全局数据没有明确的读写边界,任何模块都能改,出bug时你根本不知道是谁在哪个时间点把它改坏了。第三,测试和复用变得很麻烦,多个测试用例之间要互相清理数据,不然状态串来串去。
那用第三方全局状态库呢?当然是个思路。我在一些中型项目里用过Redux之类的方案,它把状态收敛到单一Store里,逻辑上是清晰了,但随之而来的是样板代码:定义action type、写action creator、写reducer分支、用connect或者hooks把状态映射到组件……对于几个页面共用一个用户信息、一份主题配置、一组筛选条件这种“跨层但低频”的状态,这套流程实在有点重。context-mode正好卡在props和全局状态库之间:它是框架原生的能力,不需要额外依赖,又能做到“在组件树的某个层级范围内共享状态”,改起来心智负担也小得多。
1.3 什么场景最适合用context-mode
后来我在实际项目里总结出了一个判断标准:跨层级共享、更新频率不高的数据,最适合用context-mode。典型的有三类。
一是“用户态数据”:登录用户的id、昵称、头像、权限标记。这类数据在应用启动时加载一次,之后偶尔更新,几乎所有深层组件都要读。二是“UI偏好数据”:主题色、语言、暗黑模式开关、侧边栏折叠状态。它会在用户操作时变化,但频率不高,而且天然就是全局性的。三是“领域配置数据”:比如功能开关、后端接口地址、当前租户编号,基本是应用启动时就定好的,不会变。
相反,如果数据每秒变好几次,而且要驱动列表局部更新、表单输入、动画帧这种高频场景,直接把整个对象塞进Context里,很容易引发大面积重渲染,这种情况下就应该考虑更细粒度的方案。这个边界问题我后面单独讲。
2. 先理解context-mode的通信模型和设计原则
2.1 Provider、Consumer、createContext是怎么协作的
很多资料把Context描述成“跨组件传值”,这个说法没错,但容易让人误以为它和props是一类东西。我的理解更偏向一个比喻:Context是一条数据通道,Provider是广播站,useContext是收音机。
组件树本身是静态的:父组件渲染子组件,子组件渲染孙组件,信息只能从上往下走。context-mode做的事,是在组件树的某一层架设一条“侧信道”,让上层组件把数据放进去,下层组件无论嵌套多深,只要调一下useContext就能直接取到,不需要中间层的组件帮忙搬运。
具体到React里,createContext会返回一个对象,其中包含Provider和Consumer两个能力。把某个组件包在<SomeContext.Provider value={...}>里面之后,这个组件树中所有调用useContext(SomeContext)的子组件,都能拿到value。取用关系类似于“广播台覆盖范围”,子组件不关心广播站在哪里,只要自己在覆盖范围内就能收到信号。这就是为什么它能解决props钻透的根本原因:中间组件不再需要参与数据搬运。
2.2 “mode”到底是什么
我第一次看到“context-mode”这个词,其实是一份项目文档里的命名,项目里管它叫一种“上下文模式”。我当时就顺着这个词往下想:它并不是某个新框架或新API,而是一种组织状态的约定。就和“controller模式”“hooks模式”一样,它是一套写代码的套路,核心是把Context当作状态通道来组织应用里的共享数据。
在这套套路里,你会看到几种典型形态。最基础的是“单一Context”:整个应用一个Provider,内部塞一个大对象,简单但容易引发全局重渲染。进阶一点的是“多Context拆分”:用户信息一个Context,主题一个Context,接口配置一个Context,各自维护,互不干扰,更新其中一个不会让所有消费者都重渲染。再进一步是“分层Context”:外层Provider只放稳定数据,内层Provider放变化频繁的数据,尽量把变化范围收缩到局部。
到了2020年前后,社区里开始流行一种做法,把useReducer和Context组合起来,形成“reducer + context”的模式:Provider里挂一个reducer,dispatch作为发布动作的入口。这就是后来越来越多人说的“context-mode”。所以如果你想在技术评审里向别人解释这个词,最准确的说法是:一种基于Context通道、配合状态reducer/action组织方式的前端状态管理模式。
2.3 value的稳定性是核心中的核心
在我看过的所有context-mode翻车案例里,有将近一半的问题出在同一个地方:Provider的value一直在变。
规则其实很朴素:只要value引用变化了,所有消费者都会重新渲染。注意,是引用变化,不是内容变化。如果你在Provider的函数体里直接写value={{ state, dispatch }},那每次组件render都会生成一个新对象,哪怕state和dispatch实际上没变,所有useContext到这个对象的子组件也会被强制刷新。这就是性能问题的根源之一。
解决办法是useMemo(在React 19里编译器会帮你做一部分,但习惯还是要养好):把value包一层,只有当state真正变化时才生成新对象。dispatch本身在React里是稳定的,不会变,所以依赖数组里写state就够了。记住这句话:value的稳定程度,决定了整个context-mode的性能上限。
2.4 四条设计原则
做了几个项目之后,我把context-mode的设计原则总结成四条,基本可以覆盖大部分业务场景。
第一,最小作用域原则。Provider别乱放,不要一上来就把整个应用包住。只有某个子树里的组件需要共享数据,就把Provider放在那个子树的根部。比如筛选条件只影响列表页,那就把它包在列表页里,而不是根组件里。
第二,上下文拆分原则。不同类型的共享数据,拆成不同的Context。用户状态、主题状态、路由状态、接口配置,分开管理。这不仅能减少无关注册,还能让模块边界更清楚。
第三,数据下沉原则。能放在本地state的,不要放进Context;能放在props里的,不要放进Context。Context是解决跨层共享问题的,不是替代所有组件通信的。如果数据只在一个组件内部使用,放在本地state里是最优解。
第四,默认值兜底原则。createContext需要给一个默认值。很多团队习惯传null,然后在hooks里判断并抛错,这确实能帮助及早发现“忘了包Provider”的问题。但如果业务上有明确的初始值,比如空列表、默认主题,直接给一个合理默认值更稳妥,也能让组件在测试和预览时不至于崩溃。
3. 实操:手写一个可复用的context-mode状态模块
3.1 模块结构
这部分我会用一个“任务清单”的例子,带着你从零搭一个干净的context-mode状态模块。目录结构我建议这样组织:
src/ features/ task/ TaskContext.tsx // Provider、Context、hooks 都放这里 taskReducer.ts // reducer 和 action 创建函数 types.ts // Task、TaskState、TaskAction 的类型 useTask.ts // 给业务组件使用的自定义hooks这个拆分方式,是把“状态逻辑”和“UI组件”分开。TaskContext.tsx里只负责创建Context、封装Provider、导出hooks;taskReducer.ts里只负责定义状态更新逻辑。这样业务组件只需要import { useTask } from './useTask',完全不用关心底层用的是useReducer还是setState。
3.2 定义State和Action
先看types.ts。这个模块我打算管理两个字段:任务列表,以及筛选条件。
export type Task = { id: string; text: string; done: boolean; }; export type Filter = 'all' | 'active' | 'done'; export type TaskState = { tasks: Task[]; filter: Filter; }; export type TaskAction = | { type: 'ADD_TASK'; text: string } | { type: 'TOGGLE_TASK'; id: string } | { type: 'SET_FILTER'; filter: Filter };action类型写完整,IDE补全和后续维护都会舒服很多。很多人刚开始用useReducer时喜欢写字符串散落的action,比如dispatch({ type: 'add' }),过两天再看代码根本不知道payload里有什么。使用TypeScript的联合类型,dispatch时会强制校验action结构,能挡掉不少低级错误。
接着写reducer。reducer的职责是“根据当前状态和action,生成一个新状态”,必须是纯函数:
import { TaskState, TaskAction } from './types'; export const initialTaskState: TaskState = { tasks: [], filter: 'all', }; export function taskReducer(state: TaskState, action: TaskAction): TaskState { switch (action.type) { case 'ADD_TASK': return { ...state, tasks: [ ...state.tasks, { id: Date.now().toString(), text: action.text, done: false }, ], }; case 'TOGGLE_TASK': return { ...state, tasks: state.tasks.map((task) => task.id === action.id ? { ...task, done: !task.done } : task ), }; case 'SET_FILTER': return { ...state, filter: action.filter, }; default: return state; } }有一点必须强调:每个case里返回的必须是新对象,不能在原数组或原对象上push、splice,因为React依赖引用比较来判断状态是否变化。如果直接修改了原对象,就算state内容变了,组件也不会重新渲染,这类bug特别隐蔽。
3.3 Provider与useMemo
然后是TaskContext.tsx,这里是最关键的部分:
import { createContext, useContext, useMemo, useReducer, ReactNode } from 'react'; import { TaskState, TaskAction } from './types'; import { taskReducer, initialTaskState } from './taskReducer'; type TaskContextValue = { state: TaskState; dispatch: React.Dispatch<TaskAction>; }; const TaskContext = createContext<TaskContextValue | null>(null); export function TaskProvider({ children }: { children: ReactNode }) { const [state, dispatch] = useReducer(taskReducer, initialTaskState); const value = useMemo<TaskContextValue>( () => ({ state, dispatch }), [state] ); return <TaskContext.Provider value={value}>{children}</TaskContext.Provider>; }注意看useMemo的依赖数组里只有state,没有dispatch。这是因为React保证dispatch在组件的生命周期内是稳定不变的,你要是把dispatch写进依赖数组也不会出错,但纯属多余。value被useMemo包住后,只要state不变,Provider重新渲染时也不会生成新的value对象,所有消费者都不会被误刷。
3.4 封装hooks:对外给组件的API
Context的value直接暴露dispatch给业务组件虽然能用,但不够友好。组件层经常会写重复的dispatch({ type: 'TOGGLE_TASK', id })这种代码,而且action的type一旦改了,所有调用点都要改。实操中我更推荐在useTask.ts里做一层封装,把“动作”封装成函数:
import { useCallback } from 'react'; import { TaskContext, TaskContextValue } from './TaskContext'; export function useTask(): TaskContextValue { const ctx = useContext(TaskContext); if (!ctx) { throw new Error('useTask must be used within TaskProvider'); } return ctx; } export function useTaskActions() { const { dispatch } = useTask(); const addTask = useCallback( (text: string) => dispatch({ type: 'ADD_TASK', text }), [dispatch] ); const toggleTask = useCallback( (id: string) => dispatch({ type: 'TOGGLE_TASK', id }), [dispatch] ); const setFilter = useCallback( (filter: TaskState['filter']) => dispatch({ type: 'SET_FILTER', filter }), [dispatch] ); return { addTask, toggleTask, setFilter }; }这样封装之后,业务组件里只需要调用addTask('写博客'),从语义上理解操作,不需要管action的结构。useCallback包一下还能让这些函数在依赖数组里使用时不引发无谓的效果重跑。
3.5 页面里怎么用
业务组件端的使用方式就清爽多了:
import { TaskProvider } from './TaskContext'; import { useTask, useTaskActions } from './useTask'; function TaskList() { const { state } = useTask(); const visibleTasks = state.tasks.filter((task) => { if (state.filter === 'active') return !task.done; if (state.filter === 'done') return task.done; return true; }); return ( <ul> {visibleTasks.map((task) => ( <TaskItem key={task.id} task={task} /> ))} </ul> ); } function TaskItem({ task }: { task: Task }) { const { toggleTask } = useTaskActions(); return ( <li> <label> <input type="checkbox" checked={task.done} onChange={() => toggleTask(task.id)} /> {task.text} </label> </li> ); } export default function TaskPage() { return ( <TaskProvider> <TaskList /> </TaskProvider> ); }组件里只做两件事:读状态、调用动作函数。中间经过哪些层级、谁在传递props,都不需要关心。如果要加一个“统计未完成任务数”的组件,扔到TaskProvider底下任意位置都能直接useTask()读取,这就是context-mode带来的自由度。
3.6 测试和扩展的要点
测试context-mode组件时,最常见的坑是“组件忘包Provider就跑测试”。你可以在测试工具里给组件外面套一个TaskProvider再渲染,或者写一个renderWithTaskProvider的辅助函数。如果组件里面用了useTask()而测试环境没包Provider,useTask会抛错,反而能第一时间发现问题。
后续如果模块要扩展异步逻辑,比如从接口加载任务列表,我一般会在Provider里再维护一个loading和error状态,然后在useTaskActions里加一个fetchTasks函数,内部用AbortController支持取消请求。核心的context-mode结构不用变,只是reducer里多几个case而已。
4. 性能陷阱:context-mode不是免费的
4.1 最常见的“全局二次渲染”问题
context-mode用起来很顺手,但它有一条隐藏的性能成本:所有注册了这个Context的组件,在value变化时都会重新渲染。
举个例子:你把用户信息放进了全局UserContext里,整个应用所有读UserContext的组件对这个Context产生了依赖。某天用户改了头像,UserContext.value变化了,所有读它的组件都会重新渲染。如果你的应用里有十几个组件都在用用户信息,哪怕它们之间毫无关联,也会一起刷新。
这个行为本身是React的合理机制:Context没法做到“只通知其中几个组件”。React只知道依赖关系是“谁订阅了这个Context”,不知道它具体用了Context里的哪个字段。所以value一变,所有人都被通知。理解这一点,你就明白优化方向不是“让Context精准通知”,而是要么拆小Context,要么让value尽量不变。
4.2 优化手段1:拆分Context
拆分Context是性价比最高的优化手段。把一个大Context按业务域拆成多个独立Context,每个Context的value变化频率不一样,就能显著减少无关联组件的重渲染。
拿前面的任务清单模块举例,任务列表和筛选条件虽然属于同一个领域,但更新频率完全不同:任务列表可能每几秒变一次,筛选条件用户偶尔点一下。如果把它们放在同一个Context里,用户切换筛选器时,所有依赖任务列表的组件也会一起刷新,虽然任务列表本身没变。拆成TasksContext和FilterContext两个Context后,互不影响,筛选条件的变更只会触发用到筛选值的组件重渲染。
这种拆分说起来简单,做的时候要注意一个度:拆得太碎,Provider的嵌套层数会越来越多。我的经验是,按照“更新频率”和“业务边界”两个维度来拆,而不是按字段粒度拆。一个团队成员信息可能在多个页面共用,但也很少变化,那就合成一个Context;表单输入每秒都在变,那它就应该单独放在一个局部Context里,甚至用本地state都行。
4.3 优化手段2:useSyncExternalStore的selector模式
如果你面对的数据确实高频变化,而且真的需要细粒度的“部分更新”,那就要跳出Context本身的限制,用useSyncExternalStore来做selector订阅。这个API是React 18开始主推的,它的核心作用是让组件只订阅自己关心的那部分状态,状态变化时只有关心它的组件重新渲染。
我的做法是:先用一个外部store保存状态(可以用useReducer + 自定义store,也可以直接用状态库的store),然后给组件提供一个类似useSelector的hooks:
function useSelector<TSelected>(selector: (state: TaskState) => TSelected): TSelected { return useSyncExternalStore( store.subscribe, () => selector(store.getState()) ); }组件端就可以写const undoneCount = useSelector((s) => s.tasks.filter(t => !t.done).length)。这样即使任务列表频繁变化,只有真正使用了undoneCount的组件会刷新,其他组件不会。
注意selector返回的必须是原始值(字符串、数字),不能是一个每次调用都会新建的数组或对象,否则useSyncExternalStore比较依赖时会认为每次都变化,导致死循环。如果确实需要返回派生对象,就用useMemo包一层,或者让selector返回一个稳定引用。
4.4 排查工具与火焰图
排查性能问题时,我最常用的工具是浏览器开发者工具里的React Profiler。打开之后录一段操作,然后看火焰图里每个组件的渲染耗时和渲染原因。如果一个组件明明没收到新props,也没有本地state变化,却被标成了“因为Context而重渲染”,那基本可以确定它订阅的某个Context的value变了。
这个工具有一个很好的提示:它会列出“Props changed”还是“Context changed”。看到“Context changed”标记,我就能顺着去查对应Provider的value是否被useMemo包好了,或者是不是数据更新频率确实太高。没有这个工具,光靠肉眼猜,很容易误判成子组件自己的逻辑有问题。
5. 常见问题与排查技巧实录
5.1 useStore报错:缺少Provider
很多团队在封装context-mode时,会在useXxx的hooks里做空值判断,Context默认值是null,一旦检测到null就抛错。运行时报useTask must be used within TaskProvider,通常有三种原因。
第一,组件挂在Provider外面。最常见的是路由配置里某个页面没包Provider,或者弹窗用了另一个渲染入口。第二,测试环境里没包Provider。第三,动态挂载导致Provider和消费者不在同一棵树上,比如用了createPortal把组件渲染到body下,Portal里虽然还在Provider的“逻辑子树”内,但有些人会在文档流上把它挪到别的DOM容器里,如果Provider的层级没覆盖到,也会报错。
排查思路就是先找到报错组件的根,确认它有没有被Provider包裹。这里我建议:像TaskProvider这种领域Provider,尽量收拢在页面入口或模块根组件里,全局Provider收拢在应用根组件里,避免散落。
5.2 改了状态视图不动
典型的隐性问题。“点击按钮、dispatch也执行了、console里看state确实变了,但界面没反应。”遇到这个现象,我第一反应是看reducer里是不是改了原对象。最经典的错误写法是:
case 'TOGGLE_TASK': const task = state.tasks.find(t => t.id === action.id); task.done = !task.done; return state;这样写state的引用没变,React的比较机制认为状态没变化,组件自然不重新渲染。正确做法是返回一个新对象,像前面代码里那样用map生成新的数组。还有一个类似的情况:state对象里某层嵌套变了,但外层对象没有重新创建。React只会做浅比较,深层的引用变了但表层引用没变,一样不刷新。所以reducer里每一层变化都要产生新的引用。
5.3 所有消费者一起重渲染
这个我在前面性能部分已经展开过了。再补充一个实战排查套路:打开React DevTools,看组件树中的每个消费者组件是不是都显示“Context changed”。如果是,然后看Provider的value是不是在每次render时都重建了。如果value用useMemo包了还没用,检查依赖数组是不是把state的所有字段都拆开写了一遍——比如依赖数组写成了[state.tasks, state.filter],结果tasks虽然没变但filter每次点击都变,那value也会跟着变。依赖数组最好直接写[state],让整个state对象作为唯一依赖。
5.4 多层Context嵌套太重
业务越做越大,Provider嵌套也会越来越深。最夸张的时候我看过一个项目根组件里套了七八层Provider,光是包裹组件就有四五十行。这种嵌套会让代码很难阅读,而且排查递归渲染时非常痛苦。
我的处理方案是写一个组合Providers的辅助组件,把嵌套平铺成配置:
export function AppProviders({ children }: { children: ReactNode }) { return ( <ThemeProvider> <I18nProvider> <UserProvider> <TaskProvider>{children}</TaskProvider> </UserProvider> </I18nProvider> </ThemeProvider> ); }这只是一个语法上的简化,本质还是嵌套,但至少阅读顺序清晰了。如果Provider真的多到难以维护,我建议重新审视一下Context的拆分粒度——是不是拆得太细了?有些纯静态配置,真的不需要放进动态Context里,可以直接用模块级常量或import来替换。
5.5 闭包拿不到最新值
有一个场景很磨人:在事件监听器、定时器、或者某个第三方库的回调里读取Context状态,读到的总是旧值。原因很简单:闭包在创建时就捕获了当时的state引用,后续state更新后,闭包里的state还是旧的。
解决思路分情况。如果是定时器场景,可以在回调里通过ref来读最新值,或者把依赖项加到useEffect里让回调重建。如果是事件监听器,应该在useEffect里重新绑定,并且把Context值加到依赖数组里。如果是在某个回调里需要读取但不希望触发重订阅,那就用一个ref同步最新值:
const { state } = useTask(); const stateRef = useRef(state); stateRef.current = state; // 回调里读 stateRef.current这个技巧虽然不是context-mode特有,但在我用Context做轮询时的实践里非常常见。
5.6 常见问题速查表
| 现象 | 根本原因 | 解决方案 |
|---|---|---|
| 组件报“useTask must be used within Provider” | 组件不在Provider子树内 | 检查组件挂载位置,补包Provider |
| dispatch后视图不更新 | reducer里直接修改了原对象 | 返回新对象,不要改原state |
| 所有消费者集体重新渲染 | Provider的value每次render都新建 | 用useMemo包裹value,依赖数组写state |
| 某字段没变,相关组件还是重新渲染 | 字段所在的Context被整体触发 | 拆分Context或改用useSyncExternalStore |
| 事件回调/定时器里读到旧值 | 闭包捕获了旧state | 用ref同步最新值,或重建回调 |
| Provider嵌套层数过多 | Context拆粒度过细 | 合并相关Context,或引入状态库统一管理 |
6. context-mode的边界、对比与落地心得
6.1 同样的思路在其他框架里也有
我之前一直觉得“Context模式”是React独有的,后来发现它其实是组件树体系里的通用思路,只是不同框架名字不同。
Vue生态对应的就是provide/inject。父组件通过provide把数据抛给整棵子树,后代组件通过inject按需注入。和React的Context相比,Vue的做法更接近“依赖注入”:inject时还可以指定默认值,数据本身天然是响应式的,Vue的更新是细粒度的,不需要像React那样手动做useMemo来稳定value。这个体验差异本质上来源于两者渲染机制的不同。
Flutter里的InheritedWidget也是同类。它允许上层组件在树里暴露一个数据对象,下层组件通过context.dependOnInheritedWidgetOfExactType读取,并且框架会在数据变化时自动标记依赖过期的组件,需要重建时会重建。很多全局主题、语言包、用户状态的方案都是基于这个机制写的。
本质上,“上层提供、下层读取、隔离中间层”是组件化架构里处理跨层通信的一种经典模式。你只要理解了一种框架里的实现,换框架时思路是可以平移的,区别只在API和更新粒度上。
6.2 什么时候该切换成真正的状态库
虽然我很偏爱context-mode,但它确实有天花板。如果你发现项目已经出现了下面几个信号,就别硬撑了,考虑引入真正的状态库,或者至少用useSyncExternalStore把状态从Context里剥离出来。
一是状态更新频率极高,而且同时有大量组件需要响应。比如实时图表、协作编辑、大列表拖拽排序,这种场景下Context的全量广播机制不是不能做,但要花很多精力去做拆分和selector优化。二是状态需要跨应用、跨标签页共享。Context的状态生命周期是跟着组件树走的,页面刷新状态就没了,跨标签页协作、多实例同步这类需求已经超出它的职责。三是状态流非常复杂,包含大量派生状态、异步队列、事务回滚。状态库自带的调试工具、时间旅行、持久化中间件,会省掉很多痛苦。
我的经验是,对于中等规模的管理类项目,90%的共享状态用context-mode就能解决;剩下10%交给状态库。如果一开始就要做很复杂的状态流,那就在架构评审阶段明确分工,不要等项目写到一半再迁移。
6.3 个人落地心得
这个context-mode方案,我在好几个项目里落地过,最终形成了一套比较稳定的组合拳:全局低频状态用“多Context拆分 + useMemo稳定value”;高频局部状态用本地state;真正复杂的状态流交给状态库,同时用useSyncExternalStore做selector订阅。
我最大的体会是,context-mode真正的好处不在性能,而在心智负担。团队新成员接手项目时,只要看懂Provider在哪、Context拆了几个、useXxx返回什么,状态流基本就清楚了。它不像全局状态库那样需要学习各种概念和中间件,也不需要额外维护一份类型定义。但反过来说,正因为它太简单了,做架构的人必须主动去控制它的边界:哪些状态进Context,哪些不进,Provider放在哪一层,这些决策才是这个模式能不能跑得长久的关键。
如果你现在正被props钻透折磨,或者被全局状态库的样板代码压得喘不过气,不妨先按这篇文章的思路搭一个最小的context-mode模块试一星期。我敢说,等习惯了Provider包一层、useXxx随处调用的写法之后,你大概率就不想回去逐层传props了。