为依赖注入定义通用 Context 接口React 组合模式中的 state / actions / meta 三要素契约【免费下载链接】crmComp AI CRM is an open source, CRM designed for AI agents. Agentic-first CRM.项目地址: https://gitcode.com/gh_mirrors/crm48/crm导读在构建可扩展的 React 组件时UI 与状态实现之间的耦合是导致组件难以复用、测试和维护的根源之一。本指南基于 Vercel 组合模式规则集位于 .agents/skills/vercel-composition-patterns/rules/state-context-interface.md展开讲解如何为组件 Context 定义由state、actions、meta三个部分组成的通用接口generic interface使其成为任何 Provider 都可实现的契约——让同一套 UI 组件能够无缝对接完全不同的状态实现本地useState、全局同步状态、服务器状态等。读完本文你将掌握依赖注入式状态管理的完整落地姿势如何定义接口、如何让 UI 只消费接口、如何用不同 Provider 实现同一接口以及如何突破视觉嵌套边界共享状态。为什么需要通用 Context 接口从耦合到可注入React 组合的核心难题在于状态管理方案本地 state、全局 store、服务端同步与 UI 表现往往纠缠在一起。当组件内部直接调用某个特定 Hook如useChannelComposerState时UI 就被锁定在了单一实现上——换一种状态来源就得重写组件。规则文件给出的核心原则只有一句话Lift state, compose internals, make state dependency-injectable.提升状态、组合内部结构、让状态可依赖注入。而实现这一原则的载体就是三段式通用 Context 接口组成部分职责典型内容state只读的当前状态数据input、attachments、isSubmittingactions修改状态或触发副作用的函数集合update(updater)、submit()meta与 UI 生命周期相关的引用或元数据inputRef: React.RefObjectTextInput这套接口设计本身与状态实现完全解耦Provider 决定状态从哪来useState、Zustand、服务端同步UI 组件只看到统一的state / actions / meta形状。从源码结构看该规则属于 .agents/skills/vercel-composition-patterns 技能中状态管理State Management, HIGH优先级分类与state-lift-state状态提升和state-decouple-implementation状态与 UI 解耦共同构成一套完整的状态治理方案。反模式UI 与具体状态实现紧耦合先看一个直观的反面教材——UI 组件直接消费特定 Hookfunction ComposerInput() { // Tightly coupled to a specific hook const { input, setInput } useChannelComposerState() return TextInput value{input} onChangeText{setInput} / }这段代码的问题在于ComposerInput依赖的是useChannelComposerState()这个具体实现而非抽象契约。一旦另一个场景比如转发消息表单需要相同的输入框 UI 但使用不同的状态管理方式就必须复制组件或为其增加条件逻辑——这正是 boolean prop 泛滥、组件爆炸的起点。与之对比状态管理规则中的姊妹文档 state-decouple-implementation.md 明确指出Provider 组件应当是唯一知道状态如何被管理的地方UI 组件只消费 Context 接口不关心状态来自useState、Zustand 还是服务端同步。正模式定义三段式通用 Context 接口正确做法的第一步是定义一个任何 Provider 都能实现的通用接口// Define a GENERIC interface that any provider can implement interface ComposerState { input: string attachments: Attachment[] isSubmitting: boolean } interface ComposerActions { update: (updater: (state: ComposerState) ComposerState) void submit: () void } interface ComposerMeta { inputRef: React.RefObjectTextInput } interface ComposerContextValue { state: ComposerState actions: ComposerActions meta: ComposerMeta } const ComposerContext createContextComposerContextValue | null(null)几个值得注意的设计细节actions.update使用函数式更新器签名(state: ComposerState) ComposerState这与 ReactsetState的 updater 形式天然兼容——Provider 可以直接把setState透传为update无需任何适配层meta专门承载 ref 等与渲染树生命周期相关的对象避免把数据和DOM 引用混在同一个state里Context 的泛型参数带| null默认值配合 React 19 的use()在消费端做空值兜底。UI 组件只消费接口不依赖实现定义好接口后UI 组件通过use(ComposerContext)读取三要素function ComposerInput() { const { state, actions: { update }, meta, } use(ComposerContext) // This component works with ANY provider that implements the interface return ( TextInput ref{meta.inputRef} value{state.input} onChangeText{(text) update((s) ({ ...s, input: text }))} / ) }注意这里使用的是 React 19 的use()而不是useContext()。规则 react19-no-forwardref.md 说明了原因React 19 中use()取代useContext()且可以条件调用而useContext()不行。同一规则还指出 React 19 中ref已回归为普通 prop不再需要forwardRef包装。当前项目apps/app的移动端导航 mobile-nav.tsx正是采用createContextMobileNavContextValue | null(null)这种空值兜底模式与本规则的接口定义风格一致。不同 Provider 实现同一接口通用接口的价值在于一套 UI多个 Provider。规则文件给出了两个典型实现// Provider A: Local state for ephemeral forms function ForwardMessageProvider({ children }: { children: React.ReactNode }) { const [state, setState] useState(initialState) const inputRef useRef(null) const submit useForwardMessage() return ( ComposerContext value{{ state, actions: { update: setState, submit }, meta: { inputRef }, }} {children} /ComposerContext ) } // Provider B: Global synced state for channels function ChannelProvider({ channelId, children }: Props) { const { state, update, submit } useGlobalChannel(channelId) const inputRef useRef(null) return ( ComposerContext value{{ state, actions: { update, submit }, meta: { inputRef }, }} {children} /ComposerContext ) }Provider A 用useState管理临时表单的本地状态Provider B 用useGlobalChannel消费频道的全局同步状态。两者实现的接口完全相同——value的形状一模一样——因此下面的 UI 组合可以不加任何修改地复用在两种场景// Works with ForwardMessageProvider (local state) ForwardMessageProvider Composer.Frame Composer.Input / Composer.Submit / /Composer.Frame /ForwardMessageProvider // Works with ChannelProvider (global synced state) ChannelProvider channelIdabc Composer.Frame Composer.Input / Composer.Submit / /Composer.Frame /ChannelProvider这正是组合优于配置Composition over configuration原则的体现——规则集 README.md 中列出的四大核心原则之一State in providers, not trapped in components状态放在 Provider 中而不是困在组件里。同时Provider 作为唯一知道状态如何管理的边界也天然满足state-decouple-implementation规则换掉 Provider 内部的useState换成 Zustand 或服务端订阅UI 一行都不用改。突破视觉嵌套Provider 边界才是状态共享的边界通用 Context 接口带来的另一个关键能力是共享状态的组件不必在视觉上互相嵌套。规则明确指出The provider boundary is what matters—not the visual nesting.组件只要处于同一个 Provider 内就可以读取和修改状态无论它在 DOM 树中的视觉位置在哪里。看这个转发消息对话框的完整示例function ForwardMessageDialog() { return ( ForwardMessageProvider Dialog {/* The composer UI */} Composer.Frame Composer.Input placeholderAdd a message, if youd like. / Composer.Footer Composer.Formatting / Composer.Emojis / /Composer.Footer /Composer.Frame {/* Custom UI OUTSIDE the composer, but INSIDE the provider */} MessagePreview / {/* Actions at the bottom of the dialog */} DialogActions CancelButton / ForwardButton / /DialogActions /Dialog /ForwardMessageProvider ) } // This button lives OUTSIDE Composer.Frame but can still submit based on its context! function ForwardButton() { const { actions: { submit }, } use(ComposerContext) return Button onPress{submit}Forward/Button } // This preview lives OUTSIDE Composer.Frame but can read composers state! function MessagePreview() { const { state } use(ComposerContext) return Preview message{state.input} attachments{state.attachments} / }ForwardButton和MessagePreview都不在Composer.Frame的视觉框内却能通过use(ComposerContext)访问submit动作和state.input。这是把状态提升到 Provider带来的直接红利不需要 prop drilling不需要用 ref 在提交时偷读状态更不需要 useEffect 在每次变更时向上同步。这三种被规则明确否决的替代方案详见姊妹规则 state-lift-state.md值得引以为戒反模式问题状态困在组件内部兄弟组件无法访问必须 prop drilling 或引入 ref用useEffect把状态向上同步每次变更触发额外渲染与副作用脆弱易错提交时从 ref 读取当前状态状态与 UI 脱节时序不可靠正确解法始终是状态提升到 Provider消费方通过通用接口访问。正如规则文件结尾的总结The UI is reusable bits you compose together. The state is dependency-injected by the provider. Swap the provider, keep the UI.UI 是可复用的组合零件状态由 Provider 依赖注入换 ProviderUI 不动。与组合模式家族的协同从接口到完整架构state-context-interface并非孤立规则它是整个组合模式体系的一环。结合 .agents/skills/vercel-composition-patterns 的规则目录结构可以看到它与相邻规则的协同关系state-lift-state.md状态提升——解决状态放哪移入独立 Provider让兄弟组件无需 prop drilling 即可访问state-context-interface.md通用接口本文——解决接口长什么样state / actions / meta三段式契约让任意 Provider 可注入state-decouple-implementation.md实现解耦——解决谁管理实现只有 Provider 知道状态来自useState还是全局同步architecture-compound-components.md复合组件——解决UI 怎么拼以Composer.Provider / Composer.Frame / Composer.Input / Composer.Submit形式导出复合组件每个子组件通过共享 Context 获取状态消费者显式组合所需零件无隐藏条件分支。四条规则合在一起构成完整的闭环状态提升进 Provider → 通过通用接口注入 → UI 只依赖接口 → 以复合组件形式自由组合。当多个场景复用同一组件结构时依赖注入使得不同 Provider 可以同时服务同一套 UI互不干扰。何时使用适用场景与判断标准根据 SKILL.md 的触发条件本模式适用于以下场景重构 boolean prop 泛滥的组件——与其给Composer加showAttachments、showFormatting、showEmojis等开关不如让消费者用复合组件显式组合所需零件构建可复用组件库——需要为不同宿主环境表单、频道、对话框提供一致 UI 接口设计灵活的组件 API——让state成为可注入依赖而非组件内部秘密审查组件架构——检查 UI 是否泄漏了状态实现细节。需要说明的适用前提是示例代码中使用的use()React 19 新 API与ref作为普通 prop 的写法仅适用于 React 19如果项目仍停留在 React 18 及以下需将use(ComposerContext)换回useContext(ComposerContext)并保留forwardRef包装见 react19-no-forwardref.md 的兼容性说明。小结为 Context 定义通用的state / actions / meta三段式接口是让 React 组件从实现绑定走向契约驱动的关键一步接口即契约ComposerContextValue是任何 Provider 都能实现的协议UI 组件只依赖协议不依赖实现Provider 即注入点本地useState与全局同步状态可以无缝互换换 Provider 不动 UI边界即共享域Provider 边界而非视觉嵌套决定状态可达性让对话框底部按钮、消息预览等外围 UI 也能安全读写核心状态。这套模式让代码库对人和 AI Agent 都更易维护——这正是本规则集vercel-composition-patternsVercel 出品MIT 协议被设计用来解决的问题。遵循它你的组件将获得可插拔状态、可组合 UI、可替换实现三项长期收益。参考与延伸阅读规则原文state-context-interface.md状态提升state-lift-state.md实现解耦state-decouple-implementation.md复合组件architecture-compound-components.mdReact 19 API 说明react19-no-forwardref.md规则集总览与核心原则README.md、SKILL.md仓库内同风格 Context 实现参考mobile-nav.tsx【免费下载链接】crmComp AI CRM is an open source, CRM designed for AI agents. Agentic-first CRM.项目地址: https://gitcode.com/gh_mirrors/crm48/crm创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
