Refine v5 中的 useStepsForm用 React Hook Form 构建多步骤表单的完整实战指南【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine导读useStepsForm是 Refine 在refinedev/react-hook-form包中提供的高阶表单 Hook用于管理多步骤分步表单它替你维护当前处于哪一步的状态、提供跳转到指定步骤的方法并在步骤切换时自动触发校验。本文以仓库中 form-react-hook-form-use-steps-form 示例为骨架从 Hook 用法、完整可运行代码、配置项、返回值一直深入到 packages/react-hook-form/src/useStepsForm/index.ts 的源码实现与测试用例读完你可以在 Refine v5 React Hook Form 的 headless 项目中独立实现带校验、带回显的多步骤创建/编辑表单。一、示例速览一个三步的 Post 表单仓库中的示例文档 documentation/docs/examples/form/react-hook-form/useStepsForm.md 明确说明了本示例的核心能力useStepsFormallows you to manage a form with multiple steps. It provides features such as which step is currently active, the ability to go to a specific step and validation when changing steps etc.即跟踪当前激活步骤、可跳转到任意指定步骤、步骤切换时进行校验三大能力。示例把一篇 Post 的创建/编辑拆成了三个步骤步骤索引步骤名表单字段0Titletitle必填1Statusstatuspublished / draft / rejected2Category and contentcategory.id必填、content必填示例的依赖与运行方式见 examples/form-react-hook-form-use-steps-form/package.json核心依赖为refinedev/core^5.0.12、refinedev/react-hook-form^5.0.4、react-hook-form^7.57.0、refinedev/react-router与react-router数据源使用refinedev/simple-rest指向https://api.fake-rest.refine.dev。示例在本地运行时可直接在仓库根目录执行pnpm install pnpm --filter form-react-hook-form-use-steps-form dev二、在页面中接入 useStepsForm2.1 引入并解构 HookuseStepsForm是泛型 Hook类型参数依次是查询数据类型、错误类型和变量类型用于在编写时获得完整的类型检查。参考 examples/form-react-hook-form-use-steps-form/src/pages/posts/create.tsximport { useSelect, type HttpError } from refinedev/core; import { useStepsForm } from refinedev/react-hook-form; import { Controller } from react-hook-form; import type { IPost } from ../../interfaces; const stepTitles [Title, Status, Category and content]; export const PostCreate: React.FC () { const { refineCore: { onFinish, formLoading }, register, handleSubmit, formState: { errors }, steps: { currentStep, gotoStep }, control, } useStepsFormIPost, HttpError, IPost(); // ... };关键返回值的分工steps.currentStep当前步骤索引从0开始计数steps.gotoStep(step)程序化跳转到指定步骤refineCore.onFinish提交数据到 data provider 的处理器直接交给handleSubmit使用register、handleSubmit、formState.errors、controlReact Hook Form 的原生能力useStepsForm全量透传。IPost接口定义在 examples/form-react-hook-form-use-steps-form/src/interfaces/index.d.ts其中category是嵌套的关系型字段{ id: number }为后续演示useSelect与Controller提供了基础。2.2 按 currentStep 条件渲染表单字段拿到currentStep后用switch语句按步骤索引渲染对应的字段组。示例中把这一逻辑封装为renderFormByStep(step)函数const renderFormByStep (step: number) { switch (step) { case 0: return ( labelTitle: /label input idtitle {...register(title, { required: This field is required, })} / {errors.title span{errors.title.message}/span} / ); case 1: return ( labelStatus: /label select idstatus {...register(status)} option valuepublishedpublished/option option valuedraftdraft/option option valuerejectedrejected/option /select / ); case 2: return ( Controller namecategory.id control{control} render{({ field }) { return ( labelCategory: /label select idcategory {...field} {options?.map((category) ( option key{category.value} value{category.value} {category.label} /option ))} /select {errors.category span{errors.category.message}/span} / ); }} / br / br / labelContent: /label textarea idcontent {...register(content, { required: This field is required, })} rows{10} cols{50} / {errors.content span{errors.content.message}/span} / ); } };这里体现了两种受控方式的混用普通字段title、status、content直接用register注册并内联声明校验规则如required: This field is required嵌套关系字段category.id因为是父对象下的子属性示例使用Controllercontrol接管渲染同时从useSelect拉取分类选项options中的value/label。2.3 步骤导航栏与提交按钮在form外部渲染一组步骤按钮让用户可以直接跳转到任意步骤并用背景色高亮当前步骤if (formLoading) { return divLoading.../div; } return ( div style{{ display: flex, flexDirection: column, gap: 16 }} div style{{ display: flex, gap: 36 }} {stepTitles.map((title, index) ( button key{index} onClick{() gotoStep(index)} style{{ backgroundColor: currentStep index ? lightgray : initial, }} {index 1} - {title} /button ))} /div form autoCompleteoff{renderFormByStep(currentStep)}/form div style{{ display: flex, gap: 8 }} {currentStep 0 ( button onClick{() { gotoStep(currentStep - 1); }} Previous /button )} {currentStep stepTitles.length - 1 ( button onClick{() { gotoStep(currentStep 1); }} Next /button )} {currentStep stepTitles.length - 1 ( button onClick{handleSubmit(onFinish)}Save/button )} /div /div );导航逻辑的要点顶部按钮直接gotoStep(index)跳转到任意步骤Previous / Next基于currentStep做 ±1 跳转且只在条件满足时渲染Save只在最后一步出现调用handleSubmit(onFinish)——注意onFinish来自refineCore它会把校验通过后的表单数据交给 Refine 的 data provider 执行create变更。2.4 编辑页的差异数据回显编辑场景与创建场景几乎一致唯一关键差异是回显。参考 examples/form-react-hook-form-use-steps-form/src/pages/posts/edit.tsxconst { refineCore: { onFinish, formLoading, query }, register, handleSubmit, formState: { errors }, steps: { currentStep, gotoStep }, control, } useStepsFormIPost, HttpError, IPost(); const { options } useSelect({ resource: categories, defaultValue: query?.data?.data.category.id, pagination: { mode: server, }, });refineCore.query是 Refine 内部为edit动作发起的详情查询query?.data?.data即当前 Post 数据useSelect通过defaultValue: query?.data?.data.category.id预选当前分类分类下拉通过Controller的field加上value{query?.data?.data.category.id}显式回显普通字段title、status、content不需要手动初始化useStepsForm内部会自动把查询数据同步进 React Hook Form 的字段值见下文源码分析。列表页与路由装配分别在 examples/form-react-hook-form-use-steps-form/src/pages/posts/list.tsx 与 examples/form-react-hook-form-use-steps-form/src/App.tsxApp.tsx中注册了posts资源的list、create、edit三个路由并开启了syncWithLocation与warnWhenUnsavedChanges。三、配置项stepsProps 与 autoSaveuseStepsForm的完整配置说明位于 documentation/docs/packages/react-hook-form/use-steps-form/index.md。它继承refinedev/react-hook-form中useForm的全部能力因此refineCoreProps、autoSave等配置同样可用。3.1 stepsProps.defaultStep设置表单初始激活的步骤计数从0开始默认值为0const stepsForm useStepsForm({ stepsProps: { defaultStep: 0, }, });3.2 stepsProps.isBackValidate控制向后切换步骤时是否校验当前步骤字段默认false。为true时用户点击 Previous 也会先校验当前步骤的字段校验失败则不允许返回const stepsForm useStepsForm({ stepsProps: { isBackValidate: true, }, });源码中的默认值在 packages/react-hook-form/src/useStepsForm/index.tsconst { defaultStep 0, isBackValidate false } stepsProps ?? {};3.3 autoSave继承自 useForm编辑场景下可以开启自动保存。配置示例useStepsForm({ refineCoreProps: { autoSave: { enabled: true, // 默认 false debounce: 2000, // 默认 1000ms onFinish: (values) ({ foo: bar, ...values }), // 提交前改写数据 invalidateOnUnmount: true, // 卸载时失效 list/many/detail 查询 }, }, });按官方文档说明autoSave仅在 edit 模式下生效编辑时改动字段会按debounce延迟自动提交创建模式下仍需手动保存。onMutationSuccess/onMutationError回调也可用于在自动保存成功或失败后做额外处理。四、源码级原理useStepsForm 内部到底做了什么多步骤状态的实现非常精简全部逻辑集中在 packages/react-hook-form/src/useStepsForm/index.ts。4.1 内部状态与步骤跳转const [current, setCurrent] useState(defaultStep); const go (step: number) { let targetStep step; if (step 0) { targetStep 0; } setCurrent(targetStep); }; const gotoStep async (step: number) { if (step current) { return; } if (step current !isBackValidate) { go(step); return; } const isValid await trigger(); if (isValid) { go(step); } };可以提炼出三条可验证的跳转规则目标步骤与当前相同时直接return避免无意义的重复校验与重渲染向后跳转step current且isBackValidate为false时直接切换不做校验其余情况向前跳转或向后但开启了isBackValidate都会先调用 React Hook Form 的trigger()全量校验校验通过才真正切步——这就是切换步骤时自动校验的实现来源。另外go内部对负数步骤做了钳制gotoStep(-7)会落在第0步。4.2 编辑数据的自动回填useStepsForm通过一个useEffect在查询数据到达时把详情回填进表单useEffect(() { const data query?.data?.data; if (!data) return; const registeredFields Object.keys(getValues()); Object.entries(data).forEach(([key, value]) { const name key as PathTVariables; if (registeredFields.includes(name)) { if (!get(dirtyFields, name)) { setValue(name, value); } } }); }, [query?.data, current, setValue, getValues]);这段代码有两个值得注意的设计只回填已经注册的字段registeredFields.includes(name)避免把无关数据塞进表单状态通过dirtyFields判断用户是否已修改过该字段——用户改过的字段不会被覆盖只有未被触碰的字段才用服务器数据setValue。这正是编辑页title、content等字段无需手动初始化的原因。4.3 返回值结构useStepsForm返回useForm的全部返回值refineCore、register、handleSubmit、formState、control、trigger等并在其上追加steps命名空间steps: { currentStep: number; // 当前步骤从 0 计数 gotoStep: (step: number) void; // 程序化切换步骤 }4.4 测试用例印证packages/react-hook-form/src/useStepsForm/index.spec.ts 中的测试与上述实现一一对应defaultStep: 4时steps.currentStep初始即为4调用gotoStep(1)后currentStep变为1gotoStep(-7)时currentStep被钳制为0当defaultStep为2时调用gotoStep(2)currentStep保持2不变关于回填行为测试分别模拟了空dirtyFields与{ field2: true }两种场景断言setValue的调用次数等于数据字段总数 − 已脏字段数且只对未脏字段以(字段名, 值)形式调用——精确验证了用户已编辑字段不被覆盖的逻辑。五、类型参数速查useStepsForm的七个泛型参数均来自官方文档的 Type Parameters 表默认值列已注明泛型参数含义默认值TQueryFnData查询函数返回的数据类型需继承BaseRecordBaseRecordTError自定义错误对象需继承HttpErrorHttpErrorTVariables变更mutation函数使用的字段值类型{}TContextReact Hook FormuseForm的第二个泛型{}TDataselect函数返回的数据类型未指定时取TQueryFnDataTQueryFnDataTResponse变更函数返回的数据类型未指定时取TDataTDataTResponseError变更错误类型未指定时取TErrorTError日常用法中像示例那样只传前三个参数IPost, HttpError, IPost即可获得良好的类型推导。六、适用边界与实践建议headless 场景首选useStepsForm位于refinedev/react-hook-form不绑定任何 UI 库步骤导航按钮、布局完全由你掌控如果使用 Ant Design / MUI / MantineRefine 在 documentation/docs/examples/form/antd/useStepsForm.md、documentation/docs/examples/form/mui/useStepsForm.md、documentation/docs/examples/form/mantine/useStepsForm.md 中分别提供了对应的useStepsForm集成示例。校验粒度默认只在向前切步时触发全量校验若业务要求返回上一步也校验例如步骤间有强依赖的必填项开启isBackValidate: true。创建与编辑共用组件从示例看PostCreate与PostEdit的渲染结构完全一致可将renderFormByStep抽成共享组件仅通过refineCore.query的有无区分回显逻辑降低重复代码。关系字段回显涉及category.id这类嵌套字段时推荐ControlleruseSelect的组合useSelect的defaultValue依赖query?.data?.data需要等详情查询完成后再渲染示例中通过formLoading提前return来保证。总结useStepsForm以极少的内部状态一个useState在useForm之上叠加了步骤管理能力currentStep定位当前步骤、gotoStep负责带校验的跳转、useEffect自动完成编辑回填且不覆盖用户已改字段。结合示例 examples/form-react-hook-form-use-steps-form、Hook 文档 documentation/docs/packages/react-hook-form/use-steps-form/index.md 与源码 packages/react-hook-form/src/useStepsForm/index.ts 阅读你可以快速把多步骤表单能力复用到自己的 Refine v5 项目中。【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考