Supabase React 19 组件实战:ref 作为普通 Props 与 use() 替代 useContext() 的迁移指南
本文基于 Supabase 仓库内 Vercel 组件组合模式技能包中的规则文件 react19-no-forwardref.md,系统讲解 React 19 带来的两项 API 变化:ref 成为普通 prop(不再需要 forwardRef 包装)以及 use() 钩子取代 useContext()。Supabase 的 Studio 前端与 UI 组件包(packages/ui、packages/ui-patterns)运行在 React 19 之上,读完本文你将能在自己的 React 19 项目中直接完成组件定义与 Context 消费方式的现代化改造。
规则定位与适用范围
这条规则出自仓库中为 AI 编码助手组织的技能文档 SKILL.md。该技能包定义了四大类 React 组件组合模式,按优先级组织为:
| 优先级 | 类别 | 影响程度 | 规则前缀 |
|---|---|---|---|
| 1 | 组件架构(Compound Components 等) | HIGH | architecture- |
| 2 | 状态管理(Provider、状态提升等) | MEDIUM | state- |
| 3 | 实现模式(显式变体、children 组合等) | MEDIUM | patterns- |
| 4 | React 19 APIs | MEDIUM | react19- |
react19-no-forwardref 属于第四类,影响程度(impact)标注为 MEDIUM,描述为“更简洁的组件定义与 Context 使用方式”,标签涵盖 react19、refs、context、hooks。
适用前提(原文档的核心警告):本规则仅适用于 React 19+。如果你的项目仍在 React 18 或更早版本,应跳过这条规则,继续保留 forwardRef 与 useContext() 的既有写法。
Supabase 仓库当前满足该前提。从 pnpm-workspace.yaml 的 catalog 定义可以看到:
react: ^19.2.6
react-dom: ^19.2.6
@types/react: ^19.2.14
@types/react-dom: ^19.2.3
各应用与组件包(如 apps/studio/package.json、packages/ui/package.json)通过 "react": "catalog:" 引用这一定义,Studio 的 AGENTS.md 也明确写明技术栈为 “Next.js pages router + TanStack Start(迁移中),React 19”。因此仓库内的组件代码应当逐步向 React 19 的新 API 收敛。
变化一:ref 成为普通 prop,不再需要 forwardRef
问题写法:forwardRef 包装
在 React 18 及更早版本中,函数组件要接收 ref 必须通过 forwardRef 高阶函数把 ref 从 props 中“剥出来”:
// 不推荐(React 19 下的遗留写法)
const ComposerInput = forwardRef<TextInput, Props>((props, ref) => {
return <TextInput ref={ref} {...props} />
})
forwardRef 带来的代价是:
- 组件签名变成高阶函数形式,类型参数顺序反直觉(
forwardRef<T, P>中 ref 类型在前、props 在后); - 对外导出的组件与内部实现函数割裂,
displayName、HOC 包装等处理变繁琐; - 无法像普通 props 一样在解构、展开、泛型约束中自然地处理 ref。
正确写法:ref as a regular prop
React 19 中 ref 被提升为普通 prop,函数组件可以直接在参数里声明并接收它:
// 推荐(React 19)
function ComposerInput({ ref, ...props }: Props & { ref?: React.Ref<TextInput> }) {
return <TextInput ref={ref} {...props} />
}
关键点在于类型声明:在 props 类型上交叉一个可选的 ref?: React.Ref<TextInput>,组件既保持普通函数签名,又显式表达了 ref 的目标类型。组件内部把 ref 继续透传给底层的 TextInput,行为与 forwardRef 版本完全等价,但定义更干净。
仓库源码中的印证
从源码结构看,Supabase 的 UI 模式包中已经存在“把 ref 作为普通 prop 传递”的实现。例如 FilterBarContext.tsx 中,FilterBarHandle 相关的类型定义里直接以 props 形式声明了:
ref: React.Ref<FilterBarHandle>
这正是规则所倡导的模式:ref 不再经过 forwardRef 通道,而是像 value、onChange 一样在 props 接口中声明与流转。
同时应当注意仓库的迁移现状:packages/ui 下大量源自 shadcn/ui 的组件(如 button.tsx)仍是 forwardRef 写法。这属于历史代码,并非 React 19 项目的目标形态——forwardRef 在 React 19 中依然被保留以保证向后兼容,不会报错,只是不再必要。判断标准是:新编写或重构的组件应使用 ref-as-prop,存量代码在触及该组件时顺带迁移即可。
变化二:use() 取代 useContext()
问题写法:useContext
传统方式下,消费 Context 使用 useContext() 钩子:
// 不推荐(React 19 下的遗留写法)
const value = useContext(MyContext)
正确写法:use()
React 19 引入的通用 use() 钩子可以直接读取 Context:
// 推荐(React 19)
const value = use(MyContext)
两者在“读取 Context 当前值”这一场景下功能等价,但 use() 有一个决定性优势(原文档明确指出的能力差异):
useContext()必须无条件调用,受 React Hooks 规则约束,写在if分支里会破坏 hooks 调用顺序;use()可以被条件调用。因为它不是按调用顺序维护内部槽位的传统 hook,而是在渲染过程中“按需读取”,所以可以安全地放在条件分支内。
这意味着诸如“仅在子组件挂载某一层时才消费某个 Context”的逻辑,过去需要拆分组件或用 useContext 的 null 返回值绕行,现在可以用条件式 use(MyContext) 直接表达。
此外 use() 还统一了对 Promise 和 Context 两种异步数据的读取入口(这是 React 19 的通用设计),因此从 useContext 切换到 use 也是面向未来 API 收敛的顺势迁移。
组合迁移实践:一次改到位的组件模板
把一个典型的“包装输入框 + 消费某个 Context”的组件迁移到 React 19 风格,完整模板如下:
import { use } from 'react'
type Props = {
disabled?: boolean
placeholder?: string
}
function ComposerInput({ ref, ...props }: Props & { ref?: React.Ref<TextInput> }) {
// 变化二:use() 取代 useContext(),且允许条件调用
const settings = use(ComposerSettingsContext)
if (settings.enabled) {
// use() 可以出现在条件分支中
}
// 变化一:ref 是普通 prop,直接透传
return <TextInput ref={ref} disabled={settings.disabled ?? props.disabled} {...props} />
}
迁移时的检查清单(对应规则文件的 impact 与 tags):
- 组件是否使用了
forwardRef?改为普通函数 +refprop 类型声明; - 是否存在
useContext(...)调用?替换为use(...); - 替换后检查
ref的类型参数是否与底层 DOM 元素/句柄类型一致(如React.Ref<TextInput>); - 确认项目 React 版本 ≥ 19,否则第 3 步的
refprop 与条件式use()均不可用。
与其他组合规则的配合
react19-no-forwardref 不是孤立规则,它与 SKILL.md 中同目录的规则协同构成完整的组件设计体系:
- architecture-compound-components.md:复合组件通常依赖共享 Context 传递内部状态——Provider 一侧的
createContext保持不变,而消费侧统一改用use(); - state-context-interface.md:定义
state/actions/meta通用接口做依赖注入,读取接口时同样走use(); - patterns-children-over-render-props.md:用 children 组合替代
renderX属性,避免组件 API 随功能增长而膨胀。
换句话说,React 19 的两项 API 变化降低了“组合模式”的实施成本:ref 透传无需高阶函数、Context 读取可条件化,这让 architecture- 与 state- 类别中那些强调组合与解耦的模式在 React 19 项目中更加自然。
小结
| 维度 | React 18 及更早 | React 19+(本规则推荐) |
|---|---|---|
| 函数组件接收 ref | forwardRef<T, P>((props, ref) => ...) |
普通函数,ref 作为 props 声明 |
| 读取 Context | useContext(MyContext),必须无条件调用 |
use(MyContext),可条件调用 |
| 组件签名复杂度 | 高阶函数 + 反直觉的类型参数顺序 | 普通函数签名,类型直观 |
| 适用版本 | 全部 | 仅 React 19+,低版本项目跳过 |
对于 Supabase 这类整体运行在 React 19.2 上的仓库,这条 MEDIUM 级别的规则是组件新代码与重构的默认约定:新组件一律用 ref-as-prop 定义、用 use() 消费 Context;存量的 forwardRef / useContext 写法(可见于 packages/ui/src/components/shadcn/ui 等目录)随功能迭代逐步收敛即可。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00