Refine v5 Mantine 服务端表单验证实战:从 HttpError 到字段级错误展示
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
本篇指南以 Refine v5 的 Mantine UI 集成为主体,围绕"服务端表单验证(Server-Side Form Validation)"这一主题展开:当 dataProvider 以特定格式返回拒绝的 Promise 时,Mantine
useForm会自动把服务端错误回填到对应表单字段。读完本文你将掌握HttpError错误契约、真实 dataProvider 的配置写法、create/edit 页面的完整接入代码,以及如何按需关闭该行为。
一、什么是服务端表单验证,为什么 Refine 要内置它
与在浏览器里用 JavaScript 完成的客户端校验不同,服务端表单验证发生在后端代码中——数据先被提交到服务器,由后端完成规则校验后再决定是否入库。这是业务系统(管理后台、内部工具、B2B 应用)中不可或缺的一环,因为客户端校验可以被绕过,真正的业务约束必须由服务端强制执行。
Refine 的价值在于:它没有把"服务端返回错误"这件事留给你手动处理,而是在所有useForm衍生 hook 中开箱即用地支持服务端验证。@refinedev/mantine的useForm直接继承这一能力,你只需要保证两件事:
- dataProvider 在失败时按约定的格式返回错误(
errors字段); - 表单组件通过
getInputProps("fieldName")绑定字段。
之后,错误会从"服务器 → dataProvider →useForm→ 具体字段"自动完成传播与渲染,详见 指南文档中的 Server Side Validation 一节。
二、错误契约:HttpError 与 ValidationErrors
服务端验证能否生效,取决于 dataProvider 抛出的错误是否匹配HttpError接口。该接口定义在 packages/core/src/contexts/data/types.ts 中:
export interface ValidationErrors { [field: string]: | string | string[] | boolean | { key: string; message: string }; } export interface HttpError extends Record<string, any> { message: string; statusCode: number; errors?: ValidationErrors; }要点:
message:面向用户/日志的整体错误描述;statusCode:HTTP 状态码,通常是 400 一类的业务错误码;errors:可选但决定服务端验证是否生效的字段。它是一个"字段名 → 错误描述"的映射表,key就是表单字段名(支持"category.id"这类嵌套路径写法),value 有四种形态,下一节详述。
一个符合契约的错误对象示例(摘自 examples/server-side-form-validation-mantine/src/App.tsx):
import type { HttpError } from "@refinedev/core"; const error: HttpError = { message: "An error occurred while updating the record.", statusCode: 400, errors: { title: ["Title is required."], "category.id": ["Category is required."], status: ["Status is required."], content: { key: "form.error.content", message: "Content is required.", }, tags: ["Tags is required."], }, };errors字段是服务端验证的"开关":当errors存在时,useForm会自动把对应字段的错误信息展示在表单中;即便message存在但errors缺失,也只会走全局错误通知,而不会逐字段回填。
三、配置 dataProvider:让失败响应携带 errors 字段
为了让演示可独立运行,示例项目server-side-form-validation-mantine采用@refinedev/simple-rest作为基础 dataProvider,并覆盖create/update方法,人为模拟后端返回校验失败。这是理解"真实服务端接入点"的最小样板(App.tsx):
dataProvider={{ ...dataProvider("https://api.fake-rest.refine.dev"), // 演示如何从 API 处理错误 update: async () => { const error: HttpError = { message: "An error occurred while updating the record.", statusCode: 400, errors: { title: ["Title is required."], "category.id": ["Category is required."], status: ["Status is required."], content: { key: "form.error.content", message: "Content is required.", }, tags: ["Tags is required."], }, }; return Promise.reject(error); }, create: async () => { // 与 update 同理,返回 Promise.reject(error) }, }}在真实项目中,后端接口通常返回{ message, errors: { field: "错误原因" } }之类的 JSON。你需要在 dataProvider 的create/update等变更方法中捕获非 2xx 响应,并将其转换(或直接透传)为HttpError结构后Promise.reject。参考实现位于 packages/simple-rest 与 packages/core/src/contexts/data/types.ts。
四、错误值四种形态与字段级传播规则
ValidationErrors中每个字段的值可以是四种类型,MantineuseForm在 packages/mantine/src/hooks/form/useForm/index.ts 中通过onMutationError回调逐一处理:
| 值类型 | 示例 | 字段上展示的错误文案 |
|---|---|---|
string | "Title is required." | 原样展示 |
string[] | ["Title is required."] | 数组合并以空格连接后展示 |
boolean | true | 展示通用文案"Field is not valid." |
{ key, message } | { key: "form.error.content", message: "Content is required." } | 优先用useTranslate按key翻译,无翻译时回退到message |
核心处理逻辑如下(摘录自 useForm/index.ts):
onMutationError: (error, _variables, _context) => { if (disableServerSideValidation) { refineCoreProps?.onMutationError?.(error, _variables, _context); return; } const errors = error?.errors; for (const key in errors) { const fieldError = errors[key]; let newError = ""; if (Array.isArray(fieldError)) { newError = fieldError.join(" "); } if (typeof fieldError === "string") { newError = fieldError; } if (typeof fieldError === "boolean") { newError = "Field is not valid."; } if (typeof fieldError === "object" && "key" in fieldError) { const translatedMessage = translate(fieldError.key, fieldError.message); newError = translatedMessage; } setFieldError(key, newError); } refineCoreProps?.onMutationError?.(error, _variables, _context); },setFieldError来自 Mantine form,因此错误最终会挂载到对应的表单字段状态上;字段组件只要用getInputProps("字段名")展开,即可自动呈现红色错误提示。这也解释了为什么嵌套字段"category.id"能精准命中Select组件——getInputProps("category.id")与错误 key 使用同一套路径规则。
五、完整接入:Create / Edit 页面怎么写
1. 创建页
examples/server-side-form-validation-mantine/src/pages/posts/create.tsx 展示了创建页的标准写法:
import { Create, useForm, useSelect } from "@refinedev/mantine"; import { Select, TextInput, Text, MultiSelect } from "@mantine/core"; import MDEditor from "@uiw/react-md-editor"; import type { ITag } from "../../interfaces"; export const PostCreate: React.FC = () => { const { saveButtonProps, getInputProps, errors } = useForm({ initialValues: { title: "", status: "", category: { id: "" }, content: "", }, }); const { selectProps } = useSelect({ resource: "categories", pagination: { mode: "server" }, }); const { selectProps: tagSelectProps } = useSelect<ITag>({ resource: "tags", pagination: { mode: "server" }, }); return ( <Create saveButtonProps={saveButtonProps}> <form> <TextInput id="title" mt={8} label="Title" placeholder="Title" {...getInputProps("title")} /> <Select id="status" mt={8} label="Status" placeholder="Pick one" {...getInputProps("status")} data={[ { label: "Published", value: "published" }, { label: "Draft", value: "draft" }, { label: "Rejected", value: "rejected" }, ]} /> <Select id="categoryId" mt={8} label="Category" placeholder="Pick one" {...getInputProps("category.id")} {...selectProps} /> <MultiSelect id="tags" {...getInputProps("tags")} {...tagSelectProps} mt={8} label="Tags" placeholder="Pick multiple" defaultValue={[]} filter={(value, _selected, item) => !!item.label?.toLowerCase().includes(value) } /> <Text mt={8} weight={500} size="sm" color="#212529"> Content </Text> <MDEditor id="content" >import { Edit, useForm, useSelect } from "@refinedev/mantine"; export const PostEdit: React.FC = () => { const { saveButtonProps, getInputProps, errors, refineCore: { query: queryResult }, } = useForm({ initialValues: { title: "", status: "", category: { id: "" }, content: "", tags: [], }, }); const defaultTags = queryResult?.data?.data?.tags || []; const { selectProps } = useSelect<ICategory>({ resource: "categories", defaultValue: queryResult?.data?.data.category.id, pagination: { mode: "server" }, }); const { selectProps: tagSelectProps } = useSelect<ITag>({ resource: "tags", defaultValue: defaultTags, queryOptions: { enabled: defaultTags.length > 0 }, pagination: { mode: "server" }, }); // ...表单结构与创建页一致 return ( <Edit saveButtonProps={saveButtonProps}> {/* TextInput / Select / MultiSelect / MDEditor,与创建页相同 */} </Edit> ); };编辑页多出的三个关键点:
- 从
refineCore.query取出当前记录,用于初始化useSelect的defaultValue; tags使用defaultTags作为MultiSelect与useSelect的默认值,并通过queryOptions.enabled在有数据时才发起标签查询;- 表单字段在
useForm内部通过useEffect监听query.data,把服务端数据按initialValues的扁平化字段映射回填(见 useForm/index.ts),因此编辑页无需手动setValues。
整个示例应用的路由与Refine提供者配置见 App.tsx,其中resources声明了posts资源的 list/show/create/edit,并开启了syncWithLocation与warnWhenUnsavedChanges。
六、按需关闭服务端验证
服务端验证默认开启,但你可以通过两个途径关闭它(disableServerSideValidation参数定义于 useForm/index.ts):
1. 单个表单级别:
useForm({ disableServerSideValidation: true, initialValues: { title: "" }, });2. 全局级别:在<Refine>组件的options中统一关闭:
<Refine options={{ disableServerSideValidation: true, }} />源码中的合并逻辑为(useForm/index.ts):
const { options } = useRefineContext(); const disableServerSideValidation = options?.disableServerSideValidation || disableServerSideValidationProp;即"全局配置 OR 表单级配置",任一为true都会跳过errors字段到表单字段的传播,此时服务端错误只会通过onMutationError回调与全局错误通知体现。关闭后若仍需拿到原始错误,可在refineCoreProps.onMutationError中自行处理。
七、配套验证:指南文档与交互式示例
除本文给出的完整示例项目外,仓库还提供了可交互的演示代码:
- documentation/docs/guides-concepts/forms/index.md#server-side-validation- 给出了
HttpError契约与六套 UI(core / React Hook Form / Ant Design / Mantine / Material UI / Chakra UI)的错误传播说明; - documentation/docs/guides-concepts/forms/server-side-validation-mantine.tsx 是一个内置 Sandpack 的最小可运行示例,用
Promise.reject硬编码错误响应,覆盖products资源的创建场景; - Mantine
useForm的完整 API 说明见 useForm Hook 文档,其源码与测试位于 packages/mantine/src/hooks/form/useForm。
小结
服务端表单验证是 Refine v5 Mantine 集成中"零配置即可获得"的能力:dataProvider 按HttpError.errors契约返回错误,@refinedev/mantine的useForm在onMutationError中完成字段级错误传播,配合getInputProps自动渲染提示;对于MDEditor等非 Mantine 组件则用errors手动兜底。掌握这套模式后,你在搭建真实管理后台时无需再为"后端校验失败如何回显表单"编写任何胶水代码。
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考