深入解析 Carbon StepFlow:基于 React Context 的 Headless 分步流程工具
【免费下载链接】carbonA design system built by IBM项目地址: https://gitcode.com/GitHub_Trending/carbo/carbon
导读
StepFlow 是 IBM Carbon Design System 在@carbon/utilities-react中提供的一套无头(headless)分步流程(Stepping Flow)工具,它把“上一步 / 下一步 / 跳转步骤 / 跨步骤表单状态共享”等核心状态逻辑从具体 UI 组件中剥离出来,通过StepProvider、useStepContext与StepGroup三个资产组合成任意形态的分步体验。读完本文,你将掌握 StepFlow 的完整 API、底层状态机实现原理,并能把它嵌入到Tearsheet、Modal乃至任意自定义容器中,构建类似CreateTearsheet、CreateFullPage的分步交互。本文全部内容基于 StepFlow README 与其对应源码展开。
一、StepFlow 是什么:从"组件内置分步"到"可组合的分步状态"
在 Carbon 生态中,CreateTearsheet与CreateFullPage等组件早已内置了分步能力,但它们将分步逻辑与特定视觉组件强耦合。StepFlow 的价值在于以更可组合、更 headless 的方式交付分步状态,让开发者把分步体验嵌入到任何组件里。
根据 README 的定义,StepFlow 包含三样资产:
StepProvider:分步状态的顶层 Provider,负责托管当前步骤、总步骤数与跨步骤表单状态;useStepContext:让 Provider 树内任意组件读取/更新分步状态的 Hook;StepGroup:步骤容器,负责渲染逻辑,保证只有当前步骤存在于 DOM 中。
这套资产全部位于 packages/utilities-react/src/StepFlow 目录,模块入口 index.ts 统一导出了StepGroup、StepProvider、useStepContext及StepContextType类型。
二、核心资产逐层拆解:从源码看实现原理
1.StepProvider:状态的唯一事实来源
在 StepContext.tsx 中,StepProvider通过useState维护了三份核心状态:
| 状态 | 初始值 | 作用 |
|---|---|---|
totalSteps | 1 | 总步骤数,由StepGroup挂载后自动上报 |
currentStep | 1 | 当前激活的步骤序号(从 1 开始) |
formState | {} | 跨步骤共享的表单状态,供各步骤读写 |
同时向外暴露四个操作方法,构成完整的导航能力:
handleGoToStep(step):直接跳转到指定步骤;handleNext():currentStep + 1,进入下一步;handlePrevious():currentStep - 1,返回上一步;setTotalSteps/setFormState:标准的useState派发函数。
// 源码:packages/utilities-react/src/StepFlow/StepContext.tsx(节选) const handleGoToStep: (step) => setCurrentStep(step), const handleNext: () => setCurrentStep((step) => step + 1), const handlePrevious: () => setCurrentStep((step) => step - 1),注意handleNext/handlePrevious都采用函数式更新,确保在并发更新场景下始终基于最新状态计算,这是源码中值得借鉴的细节。
2.useStepContext:越界即报错的防御式 Hook
useStepContext的实现非常简洁,但包含一个重要的防御逻辑(StepContext.tsx):
if (context === undefined) { throw new Error('Context hook used outside of Step provider'); }当组件在StepProvider之外调用useStepContext时,会抛出Context hook used outside of Step provider错误。这一行为在 StepFlow-test.js 中有专门用例覆盖:
it('should throw error and not render anything without step state', () => { expect(() => render( <> <StepComponent invalidUse></StepComponent> <StepGroup></StepGroup> </> ) ).toThrow('Context hook used outside of Step provider'); });这保证了状态访问的安全性——任何忘记包裹StepProvider的使用都会在开发期立刻暴露。
3.StepGroup:只让当前步骤存在于 DOM
StepGroup是 StepFlow 的渲染核心(StepGroup.tsx),其工作流程分三步:
- 通过
React.Children.toArray(children)将 children 扁平化为数组——这一步会自动过滤掉条件渲染产生的 falsy 值(如false、null、undefined),因此{someCondition && <ConditionalStep />}这类写法天然安全; - 在
useEffect中调用setTotalSteps(childrenArray.length),把有效步骤数同步给StepProvider; - 根据
currentStep取出对应子元素并只返回这一个组件:childrenArray[currentStep - 1]。
由于组件从 1 开始编号(与用户心智一致),源码中用currentStep - 1做数组下标换算。当没有步骤(<StepGroup></StepGroup>)时,currentStep保持默认值1,该行为在 StepFlow-test.js 中被测试锁定。
// 源码:packages/utilities-react/src/StepFlow/StepGroup.tsx(节选) const currentStepComponent = childrenArray[currentStep - 1]; // 只渲染当前步骤 return currentStepComponent;这意味着每次切换步骤时,非当前步骤的组件会被彻底卸载而非隐藏,其局部状态自然重置,同时减少了无效 DOM 的渲染开销。
4. 状态类型契约:StepContextType
useStepContext()的返回值类型定义在 types.ts 中:
export interface StepContextType { formState: formStateType; // 跨步骤共享的表单状态 setFormState: Dispatch<SetStateAction<formStateType>>; // 更新表单状态 totalSteps: number; // 总步骤数 setTotalSteps: Dispatch<SetStateAction<number>>; // 更新总步骤数 currentStep: number; // 当前步骤(从 1 开始) handleGoToStep: (step: number) => void; // 跳转到指定步骤 handleNext: () => void; // 下一步 handlePrevious: () => void; // 上一步 }其中formStateType是一个{ [key: string]: unknown }的索引签名接口,源码注释明确指出:这个接口应由使用者扩展,以匹配自己分步体验中的具体字段——例如你的表单里有email、city、state字段,就可以扩展出对应的强类型字段。
三、完整实战:把分步体验嵌入 Tearsheet
1. 基础骨架:Provider + StepGroup 组合
README 给出的最小可用示例是:用StepProvider包裹整个分步组件,用StepGroup声明步骤序列,StepGroup之外的任何内容(如页脚按钮)会在每一步都渲染:
const Example = () => { return ( <StepProvider> <Tearsheet> <StepGroup> <Step1 /> <Step2 /> {someCondition && <ConditionalStep />} </StepGroup> </Tearsheet> </StepProvider> ); };其中{someCondition && <ConditionalStep />}依赖前面提到的Children.toArray过滤机制——条件不成立时该步骤会被自动剔除,且不影响totalSteps的正确性。
2. 步骤组件:通过 Context 读写表单状态
StepProvider内的任意组件都可以调用useStepContext()获取上下文,从而读写跨步骤的formState。README 给出了一个邮箱输入步骤的经典写法:
const Step1 = () => { const { setFormState, formState } = useStepContext(); const { email } = formState ?? {}; return ( <TextInput labelText="Email" value={email ?? ''} onChange={(e) => { setFormState((prev) => ({ ...prev, email: e.target.value, })); }} /> ); };setFormState接收函数式更新,通过展开运算符...prev保留既有字段,只合并本次变更的email——这正是"跨步骤累积表单数据"的标准模式。测试 StepFlow-test.js 验证了该行为:在步骤 1 输入Pizza后,formState即为{ email: 'Pizza' }。
3. 导航与按钮渲染:利用 Context 定制操作区
由于导航状态(currentStep、totalSteps)也在 Context 中,操作区按钮可以完全由你控制。测试文件中定义了一个StepActions无头组件,展示了最灵活的使用方式:
const StepActions = ({ buttonRenderer }) => { const state = useStepContext(); return buttonRenderer(state); };配合buttonRenderer渲染自定义按钮:
<StepActions buttonRenderer={({ currentStep, totalSteps, handleNext, handlePrevious, handleGoToStep }) => ( <> <Button kind={'ghost'} disabled={currentStep === 1} onClick={() => currentStep !== 1 && handlePrevious()}> Back </Button> <Button onClick={() => handleGoToStep(3)}>Skip</Button> <Button onClick={() => { if (currentStep !== totalSteps) { handleNext(); } }}> {currentStep === totalSteps ? 'Submit' : 'Next'} </Button> </> )} />StepFlow-test.js 覆盖了完整的导航场景:点击 Next 从步骤 1 到 2、点击 Back 回退到 1、点击 Skip 直接跳到 3,并断言currentStep的实时更新。
四、源码级示例:Carbon 官方如何用 StepFlow 构建TearsheetWithSteps
StepFlow 并非孤立工具,Carbon 官方组件TearsheetWithSteps就是它的直接消费者。在 TearsheetWithSteps.jsx 中可以看到完整的企业级用法:
1. 外层自动包裹 Provider
组件通过包装函数自动提供StepProvider,使用者完全无需手动管理分步状态:
export function TearsheetWithSteps(props) { return ( <StepProvider> <TearsheetWithStepsInner {...props} /> </StepProvider> ); }2. 内部消费全部 Context 能力
TearsheetWithStepsInner一次性解构了totalSteps、currentStep、handleNext、handlePrevious、handleGoToStep,并用它们驱动三个 UI 区域:
- 步骤指示器(ProgressIndicator):根据
currentStep计算每个ProgressStep的complete/current/disabled状态,例如complete={currentStep > 1}、disabled={currentStep < 2}; - 主内容区:
<StepGroup>中按序声明Step1(个人信息)、Step2(位置信息)、Step3(确认提交)三个步骤组件; - 页脚操作区:
Tearsheet.Footer的actions数组通过handlePrevious()、handleNext()驱动前进后退,在最后一步将按钮文案切换为Submit:
label: currentStep === totalSteps ? 'Submit' : 'Next', onClick: () => { if (currentStep === totalSteps) { // 提交逻辑:提示成功 → 1 秒后关闭并回到第一步 } else { handleNext(); } },关闭(onClose与 Cancel 按钮)时都会调用handleGoToStep(1)重置到第一步,保证下次打开时从起点开始。
3. 步骤内部如何使用 Context
三个步骤组件均通过useStepContext()读写formState:Step1存储并校验email,Step2存储city/state,Step3用CodeSnippet以JSON.stringify(formState, null, 2)展示用户提交的全部信息——这正是"跨步骤共享表单状态"最有说服力的落地演示。
此外该组件还提供了辅助函数useStepFocus(selector),在切换步骤时通过document.querySelector(selector)?.focus()自动聚焦到该步骤的首个输入框,弥补了StepGroup卸载/挂载组件带来的焦点丢失问题,可作为无障碍实践参考。
五、使用要点与边界
结合 README、源码与测试,使用 StepFlow 时应注意以下几点:
- Provider 必须在外层:
StepGroup及任何调用useStepContext的组件必须位于StepProvider树内,否则抛出Context hook used outside of Step provider; - 步骤从 1 开始计数:
currentStep的初始值和childrenArray[currentStep - 1]的下标换算都基于"从 1 开始"的约定,条件渲染的步骤同样计入totalSteps; StepGroup只渲染当前步骤:非当前步骤组件会被卸载,其局部useState状态会丢失,跨步骤数据请统一放入formState;formStateType可扩展:建议按业务字段扩展该类型以获得类型安全;- 导航与 UI 完全解耦:
StepProvider不渲染任何 DOM,按钮、进度条、步骤容器都可以按需自由组合,这也正是"headless"定位的体现。
六、小结
StepFlow 用约一百行源码,把分步流程中最容易重复实现的"当前步骤状态 + 跨步骤表单共享 + 导航操作"抽象为一套可复用的 Context 资产。它既能支撑CreateTearsheet、CreateFullPage这类内置分步组件,也能通过 README 中的组合模式嵌入任何自定义容器;测试文件 StepFlow-test.js 覆盖了渲染、状态更新、边界报错与完整导航链路,官方示例 TearsheetWithSteps.jsx 则给出了生产级的集成范式。理解并掌握这套工具,你就能在自己的 Carbon 应用中低成本地构建一致、可访问的分步体验。
【免费下载链接】carbonA design system built by IBM项目地址: https://gitcode.com/GitHub_Trending/carbo/carbon
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考