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 的创建/编辑拆成了三个步骤:
| 步骤索引 | 步骤名 | 表单字段 |
|---|---|---|
| 0 | Title | title(必填) |
| 1 | Status | status(published / draft / rejected) |
| 2 | Category and content | category.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二、在页面中接入 useStepsForm
2.1 引入并解构 Hook
useStepsForm是泛型 Hook,类型参数依次是查询数据类型、错误类型和变量类型,用于在编写时获得完整的类型检查。参考 examples/form-react-hook-form-use-steps-form/src/pages/posts/create.tsx:
import { 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, } = useStepsForm<IPost, HttpError, IPost>(); // ... };关键返回值的分工:
steps.currentStep:当前步骤索引,从0开始计数;steps.gotoStep(step):程序化跳转到指定步骤;refineCore.onFinish:提交数据到 data provider 的处理器,直接交给handleSubmit使用;register、handleSubmit、formState.errors、control:React 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 ( <> <label>Title: </label> <input id="title" {...register("title", { required: "This field is required", })} /> {errors.title && <span>{errors.title.message}</span>} </> ); case 1: return ( <> <label>Status: </label> <select id="status" {...register("status")}> <option value="published">published</option> <option value="draft">draft</option> <option value="rejected">rejected</option> </select> </> ); case 2: return ( <> <Controller name="category.id" control={control} render={({ field }) => { return ( <> <label>Category: </label> <select id="category" {...field}> {options?.map((category) => ( <option key={category.value} value={category.value}> {category.label} </option> ))} </select> {errors.category && <span>{errors.category.message}</span>} </> ); }} /> <br /> <br /> <label>Content: </label> <textarea id="content" {...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因为是"父对象下的子属性",示例使用Controller+control接管渲染,同时从useSelect拉取分类选项(options中的value/label)。
2.3 步骤导航栏与提交按钮
在<form>外部渲染一组步骤按钮,让用户可以直接跳转到任意步骤,并用背景色高亮当前步骤:
if (formLoading) { return <div>Loading...</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 autoComplete="off">{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.tsx:
const { refineCore: { onFinish, formLoading, query }, register, handleSubmit, formState: { errors }, steps: { currentStep, gotoStep }, control, } = useStepsForm<IPost, 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.tsx:App.tsx中注册了posts资源的list、create、edit三个路由,并开启了syncWithLocation与warnWhenUnsavedChanges。
三、配置项:stepsProps 与 autoSave
useStepsForm的完整配置说明位于 documentation/docs/packages/react-hook-form/use-steps-form/index.md。它继承@refinedev/react-hook-form中useForm的全部能力,因此refineCoreProps、autoSave等配置同样可用。
3.1 stepsProps.defaultStep
设置表单初始激活的步骤,计数从0开始,默认值为0:
const 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.ts:
const { 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 Path<TVariables>; 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变为1; gotoStep(-7)时currentStep被钳制为0;- 当
defaultStep为2时调用gotoStep(2),currentStep保持2不变; - 关于回填行为,测试分别模拟了空
dirtyFields与{ field2: true }两种场景,断言setValue的调用次数等于"数据字段总数 − 已脏字段数",且只对未脏字段以(字段名, 值)形式调用——精确验证了"用户已编辑字段不被覆盖"的逻辑。
五、类型参数速查
useStepsForm的七个泛型参数(均来自官方文档的 Type Parameters 表,默认值列已注明):
| 泛型参数 | 含义 | 默认值 |
|---|---|---|
TQueryFnData | 查询函数返回的数据类型,需继承BaseRecord | BaseRecord |
TError | 自定义错误对象,需继承HttpError | HttpError |
TVariables | 变更(mutation)函数使用的字段值类型 | {} |
TContext | React Hook FormuseForm的第二个泛型 | {} |
TData | select函数返回的数据类型;未指定时取TQueryFnData | TQueryFnData |
TResponse | 变更函数返回的数据类型;未指定时取TData | TData |
TResponseError | 变更错误类型;未指定时取TError | TError |
日常用法中,像示例那样只传前三个参数(<IPost, HttpError, IPost>)即可获得良好的类型推导。
六、适用边界与实践建议
- headless 场景首选:
useStepsForm位于@refinedev/react-hook-form,不绑定任何 UI 库,步骤导航按钮、布局完全由你掌控;如果使用 Ant Design / MUI / Mantine,Refine 在 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这类嵌套字段时,推荐Controller+useSelect的组合;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),仅供参考