Redwood 表单指南:基于 React Hook Form 的 `@redwoodjs/forms` 完整实战
2026/9/24 4:13:23 网站建设 项目流程
  • 后端
  • 前端
  • Web框架
  • 开发工具

【免费下载链接】redwood

RedwoodGraphQL

项目地址:https://gitcode.com/gh_mirrors/re/redwood
点击查看免费下载

Redwood 在@redwoodjs/forms包中提供了一系列表单辅助组件,它们是对 React Hook Form(RHF)的轻量封装,让你用声明式的 JSX 就能完成校验、错误样式、服务端错误展示与类型转换。本文以 Redwood 官方文档(docs/versioned_docs/version-5.x/forms.md)为骨架,结合packages/forms/src下的真实源码实现,系统讲解每个组件的用法、validation/emptyAs等核心 Props 的行为细节,以及如何通过useRegisteruseErrorStyles和 RHF 的Controller打造自定义字段,读完即可在 Redwood 项目中写出既符合 GraphQL 类型要求又具备完整校验与错误反馈的表单。

核心思路:Redwood 表单 = React Hook Form + GraphQL 友好的增强

Redwood 的表单辅助组件绝大多数都是 React Hook Form 的简单封装,目标是让常用场景更简单。如果内置辅助组件不够灵活,你可以直接使用 React Hook Form——@redwoodjs/forms重新导出了它的全部导出(见 index.tsx 中的export * from 'react-hook-form'):

import { useForm, useFormContext, // 或者 React Hook Form 导出的任何其他内容 } from '@redwoodjs/forms'

与此同时,Redwood 在 RHF 之上做了针对 GraphQL 的扩展。源码注释(coercion.ts)明确指出:RHF 原生提供valueAsDatevalueAsNumber,但 Redwood 因后端使用 GraphQL,额外增加了valueAsBooleanvalueAsJSON两个类型转换辅助项,加上emptyAs空值处理 Props,构成了@redwoodjs/forms独有的三个能力(见 index.tsx 的模块注释)。

组件全景概览

@redwoodjs/forms导出的核心组件如下(来源于文档的组件表,并可在 index.tsx 的导出列表中一一对应验证):

组件说明
<Form>包裹所有表单组件,提供表单上下文与错误上下文
<FormError>展示服务端返回的错误信息,通常放在表单顶部
<Label>替代 HTML<label>,接受错误样式相关 Props
<InputField>替代 HTML<input>,接受校验与错误样式 Props
<SelectField>替代 HTML<select>,接受校验与错误样式 Props
<TextAreaField>替代 HTML<textarea>,接受校验与错误样式 Props
<FieldError>当同name的字段存在校验错误时展示错误消息,否则不渲染
<Submit>替代<button type="submit">,触发校验与提交(执行传给<Form>onSubmit

所有 HTML<input>类型都有对应的命名组件,命名规则为<TypeField>,其中Type为 HTML input 类型之一。文档列出的完整列表为:

<ButtonField><CheckboxField><ColorField><DateField><DatetimeLocalField><EmailField><FileField><HiddenField><ImageField><MonthField><NumberField><PasswordField><RadioField><RangeField><ResetField><SearchField><SubmitField><TelField><TextField><TimeField><UrlField><WeekField>

从源码看,这些命名组件并非逐个手写,而是在 InputComponents.tsx 中维护了一个INPUT_TYPES常量数组(其中不含checkbox,因为复选框有单独的 CheckboxField.tsx 实现),再用pascalcase元编程批量生成(INPUT_TYPES.forEach(...),见 InputComponents.tsx),每个组件本质上都是<InputField type={...} />的预置版本。

校验与错误样式 Props

所有以Field结尾的组件(即全部输入字段,以及<SelectField><TextAreaField>)都接受三类 Props:

  • validation:接受 React Hook Formregister的全部选项,外加 Redwood 独有的类型转换辅助项valueAsBooleanvalueAsJSON。对应源码类型为RedwoodRegisterOptions(coercion.ts);
  • errorClassName/errorStyle:字段出错时替换className/style使用的类名与内联样式。

name外,传给这些组件的其他 Props 都会透传给其渲染的原生标签。参考表:

Prop说明
name字段名称,React Hook Form 以它作为 key 把所有逻辑串联起来
validation所有校验逻辑,接受 RHF 的register选项与 Redwood 扩展项
errorClassName出错时应用的类名
errorStyle出错时应用的样式

典型示例:一个带校验的联系表单

import { Form, Label, TextField, TextAreaField, FieldError, Submit, } from '@redwoodjs/forms' const ContactPage = () => { const onSubmit = (data) => { console.log(data) } return ( <Form onSubmit={onSubmit}> <Label name="name" className="label" errorClassName="label error" /> <TextField name="name" className="input" errorClassName="input error" validation={{ required: true }} /> <FieldError name="name" className="error-message" /> <Label name="email" className="label" errorClassName="label error" /> <TextField name="email" className="input" errorClassName="input error" validation={{ required: true, pattern: { value: /[^@]+@[^\.]+\..+/, }, }} /> <FieldError name="email" className="error-message" /> <Label name="message" className="label" errorClassName="label error" /> <TextAreaField name="message" className="input" errorClassName="input error" validation={{ required: true }} /> <FieldError name="message" className="error-message" /> <Submit className="button">Save</Submit> </Form> ) }

注意:validation={{ required: true }}时 RHF 默认的错误消息会由<FieldError>按错误类型补全(详见下文<FieldError>小节),而带pattern等自定义校验时建议通过validation中的message字段或直接借助默认文案。

<Form>:表单上下文的总入口

凡是要让 Redwood 在校验失败时改变样式、在提交时执行回调的表单,都应该被<Form>包裹。

Prop说明
config接受一个对象,内容为 React Hook FormuseForm钩子的选项
formMethodsuseForm返回的函数集合。仅当你需要访问useForm返回的某个函数(如下例的reset)时才需要
onSubmit校验成功后调用的函数,入参为包含表单所有字段 name-value 对的对象

其他所有 Props 都会透传给其渲染的<form>标签。

<Form>的内部机制

从 Form.tsx 的实现可以看到,<Form>实际封装了三层:

  1. 内部调用useForm(config)创建表单实例(若传入了formMethods则优先使用外部实例);
  2. errorProps 中 GraphQL 错误扩展里的字段消息(errorProps?.graphQLErrors?.[0]?.extensions?.properties?.messages)注入ServerErrorsContext,供useErrorStyles消费;
  3. 通过<FormProvider>把表单实例下发给任意深度的子组件,使所有辅助组件能用useFormContext拿到register等函数。

理解这一点很重要:RHF 走的是非受控组件路线,register是它的核心钩子函数,作用是把字段"注册"进表单以便校验。Redwood 的辅助组件无法直接从<Form>逐层拿到register(组件可能嵌套任意深度),所以借助<FormProvider>上下文传递,这正是 useRegister.ts 中const { register } = useFormContext()的由来。

使用formMethods访问useForm返回函数

useForm返回的一些函数(比如重置表单的reset)在组件内部很有用。要访问它们,需要自己调用useForm,但同时仍要把返回值传给<FormProvider>,让 Redwood 的辅助组件能够注册自身:

import { useForm } from 'react-hook-form' const ContactPage = () => { const formMethods = useForm() const onSubmit = (data) => { console.log(data) formMethods.reset() } return ( <Form formMethods={formMethods} onSubmit={onSubmit}> {/* 依旧正常工作 */} <TextField name="name" validation={{ required: true }} /> </Form> ) }

configformMethods的区别在于:config是传给内部useForm的选项对象(如{ mode: 'onBlur' }),而formMethods是你自己调用useForm拿到的完整返回对象,直接接管表单实例。

<FormError>:展示服务端错误

<FormError>会渲染一个<div>,内含一个"标题"消息和一个<ul>,逐条列出服务端在保存表单时报告的错误。如果你提交了一个侥幸通过客户端校验的表单,在 scaffold 生成的页面中就能看到它的表现。

例如,下面的表单里<TextField>没有配置任何校验:

import { useMutation } from '@redwoodjs/web' const CREATE_CONTACT = gql` mutation CreateContactMutation($input: ContactInput!) { createContact(input: $input) { id } } ` const ContactPage = () => { const [create, { loading, error }] = useMutation(CREATE_CONTACT) const onSubmit = (data) => { create({ variables: { input: data }}) } return ( <Form onSubmit={onSubmit}> <FormError error={error} /> {/* 没有校验——什么邮箱都能提交! */} <TextField name="email" /> </Form> ) }

客户端没有校验,什么都能提交;但 GraphQL 是强类型系统,不会放行非法数据。它会抛出错误,通过useMutation返回的error对象冒泡到顶部,由<FormError>渲染出类似内容:

<div> <p> Can't create new contact: </p> <ul> <li> email is not formatted like an email address </li> </ul> </div>

从源码(FormError.tsx)看,<FormError>的解析逻辑很有层次:

  • 若没有error,直接返回null
  • 若存在 GraphQL 错误,顶部消息取graphQLErrors[0].message,并且当extensions.code === 'BAD_USER_INPUT'(Service Validation 错误)时,标题会被覆盖为"Errors prevented this form from being saved",字段级消息则从extensions.properties.messages中展开;
  • 若无 GraphQL 错误但有网络错误(networkError),会尝试解析bodyText(Server Parse Error)或result.errors(Server Error)中的消息。

它还支持wrapperClassNametitleClassNamelistClassNamelistItemClassName及对应 Style Props,用于整体控制错误块的样式。

<Label>:可感知错误的标签

<Label>渲染 HTML<label>标签,并根据关联字段是否存在校验错误切换classNamestyle。它既可以自闭合(此时name即标签文本):

<Label name="name" className="input" errorClassName="input error" /> <!-- 渲染为:<label for="name" class="input">name</label> -->

也可以写成成对标签并放入自定义文本,此时该文本即为渲染出的标签文字:

<Label name="name" className="input" errorClassName="input error">Your Name</Label> <!-- 渲染为:<label for="name" class="input">Your Name</label> -->

除下表列出的 Props 外,其余 Props 全部透传给底层<label>

Prop说明
name关联字段名,应与对应输入字段的name一致
errorClassNamename字段存在校验错误时使用的类名
errorStylename字段存在校验错误时使用的样式

实现上,Label.tsx 会渲染<label htmlFor={name}>htmlFor与输入字段的id(默认等于name,见 InputComponents.tsx)配对,保证点击标签可聚焦输入框;错误样式的切换则复用useErrorStyles

输入字段(Input Fields)

输入框是绝大多数表单的骨干。虽然你可以用<InputField>typeProps 做出各种输入框,但通常直接使用命名输入字段更省事——它们针对特定类型预置了合适的默认行为(尤其是类型转换)。

默认类型转换(Coercion)

以下字段会自动做类型转换,你也可以随时用validation里的setValueAs覆盖或手动实现:

字段默认转换
<CheckboxField>valueAsBoolean
<NumberField>valueAsNumber
<DateField>valueAsDate
<DatetimeLocalField>valueAsDate

valueAsDatevalueAsNumber是 React Hook Form 内置的、基于 HTML 标准的转换。但因为 Redwood 后端使用 GraphQL,表单提交的类型必须与 GraphQL 服务端期望一致。为避免用户大量手写setValueAs,Redwood 在 RHF 的valueAs系列之上额外提供了两个便捷项:

  • valueAsBoolean
  • valueAsJSON

源码层面,setCoercion(coercion.ts)负责整个转换流程的编排:当设置了valueAsJSON时,会把校验函数替换为JSONValidation(解析失败返回NaN时判为无效),并删掉valueAsJSON标记;当类型为date/datetime-local或显式设置valueAsNumber时也分别走对应分支;最终统一把转换逻辑包装成 RHF 的setValueAs函数。

空输入值的默认处理

Redwood 对空输入值提供了灵活的处理机制,合理的空值语义能让数据库关联字段的处理变得更简单。空字段行为由以下优先级规则决定:

  1. 若用户指定了setValueAs,由该函数决定空字段行为;
  2. 若设置了emptyAsProp,则emptyAs决定空字段的值(取值见下文);
  3. 若设置了validation = { required: true },空字段返回null——同时 RHF 的required校验会生效,阻止空值表单提交;
  4. 若字段是 Id 字段(name"Id"结尾),空字段返回null,这对大多数数据库关联字段是最合适的值;需要其他值时可使用emptyAs覆盖;
  5. 若以上均不适用,空字段按字段类型采用默认值:
    • DateFields →null
    • NumberFields →NaN
    • TextFields withvalueAsNumberNaN
    • SelectFields withvalueAsNumberNaN
    • SelectFields withoutvalueAsNumber''(空字符串)
    • TextFields withvalueAsJSONnull
    • TextFields 及同类 →''(空字符串)

这套决策链在源码中有完整对应实现:coercion.ts 的注释详细记录了上述 5 条规则,getSetValueAsFn(coercion.ts)则按emptyAsrequiredisId(即name.endsWith('Id'))组合出具体的转换函数,SET_VALUE_AS_FUNCTIONS表中为valueAsDatevalueAsJSONvalueAsNumbervalueAsString四种类型各自实现了emptyAsNullemptyAsUndefinedemptyAsZeroemptyAsStringemptyAsNaN等变体。

emptyAs Prop

emptyAs允许用户在未指定setValueAs的情况下,覆盖字段为空时的默认返回值。可选值有:

  • null
  • 'undefined'
  • 0
  • ''(空字符串)

例如:

<NumberField name="quantity" emptyAs="undefined" /> <NumberField name="score" emptyAs={null} />

第一个字段为空时返回undefined,第二个字段为空时返回null

需要注意的是(源码 coercion.ts 有明确说明):RHF 的setValueAs对复选框不生效,因此 Redwood 目前不为<CheckboxField>提供emptyAs功能;同时若valueAsNumberemptyAs同时使用,Redwood 会删除valueAsNumber,改由内部的setValueAs完成等价转换(coercion.ts),以保证emptyAs的优先级。

自定义输入字段(useRegister 与 useErrorStyles)

你可以通过 Redwood 提供的useRegisteruseErrorStyles两个 Hook 创建与 Redwood 体系无缝集成的自定义字段:

  • useRegister:把字段注册进 React Hook Form,是 RHFregister的包装(useRegister.ts)。它会调用setCoercion注入默认转换,合并onBlur/onChange回调,并返回可直接展开到原生元素上的{ ref, onBlur, onChange, ... };若缺失name会抛出错误('name' prop must be provided)。
  • useErrorStyles:为自定义字段建立错误样式。其实现(useErrorStyles.ts)会读取useFormContextformState.errorsServerErrorsContext中的服务端错误;若存在服务端错误,还会通过setError(name, { type: 'server', message })把它写入表单状态;当字段存在校验错误时,用errorClassName/errorStyle替换className/style

两者结合即可创建既复刻 Redwood 字段行为、又包含自定义领域逻辑的字段。下面是一个集标签、输入框、错误展示于一体的"必填"自定义字段:

import { FieldError, useErrorStyles, useRegister } from '@redwoodjs/forms' const RequiredField = ({ label, name, validation }) => { const register = useRegister({ name, validation: {...validation, required: true} }) const { className: labelClassName, style: labelStyle } = useErrorStyles({ className: `my-label-class`, errorClassName: `my-label-error-class`, name, }) const { className: inputClassName, style: inputStyle } = useErrorStyles({ className: `my-input-class`, errorClassName: `my-input-error-class`, name, }) return ( <> <label className={labelClassName} style={labelStyle}>{label}</label> <input className={inputClassName} style={inputStyle} type="text" {...register} /> <FieldError name={name} /> </> ) }

受控组件字段(结合 RHF 的 Controller)

如果你在使用功能完整的组件库,或拥有自研的生产级组件,可以通过 Redwood 的useErrorStyles配合 React Hook Form 的Controller组件将它们无缝集成进 Redwood 表单。下面是用primereactToggleButton封装成 Redwood 风格命名字段的示例(web/src/components/ToggleButtonField/ToggleButtonField.tsx):

import { ToggleButton } from 'primereact/togglebutton' import type { ToggleButtonProps } from 'primereact/togglebutton' import { Controller, RegisterOptions, useErrorStyles } from '@redwoodjs/forms' interface Props extends ToggleButtonProps { validation?: RegisterOptions errorClassName?: string } const ToggleButtonField = (props: Props) => { const { name, className, errorClassName, defaultValue, validation, style, ...propsRest } = props const { className: componentClassName, style: componentStyle } = useErrorStyles({ className: className, errorClassName: errorClassName, name: name, }) return ( <Controller name={name} defaultValue={defaultValue} rules={validation} render={({ field: { onChange, onBlur, value, name, ref } }) => ( <ToggleButton {...propsRest} checked={value} onChange={onChange} onBlur={onBlur} ref={ref} name={name} className={componentClassName} style={{ ...componentStyle, ...style }} /> )} /> ) } export default ToggleButtonField

核心要点:Controller负责把受控组件的值、变更回调与表单状态绑定,useErrorStyles负责错误时切换样式,rules={validation}把校验规则交给 RHF 执行。这样封装出来的组件在使用上与@redwoodjs/forms的命名输入字段完全一致。

<SelectField>:下拉选择框

<SelectField>渲染 HTML<select>标签,支持用multipleProps 多选。当multipletrue时,字段返回的值数组按选项在列表中的顺序排列,而不是用户选择的顺序:

<SelectField name="toppings" multiple={true}> <option>'lettuce'</option> <option>'tomato'</option> <option>'pickle'</option> <option>'cheese'</option> </SelectField> // 如果用户选择了 lettuce、tomato 和 cheese, // onSubmit 处理器收到: // // { toppings: ["lettuce", "tomato", "cheese"] }

校验

下面的两个例子分别演示了单选与多选场景下"必须选择且不能选第一个占位选项"的校验:

<SelectField name="selectSingle" validation={{ required: true, validate: { matchesInitialValue: (value) => { return ( value !== 'Please select an option' || 'Select an Option' ) }, }, }} > <option>Please select an option</option> <option>Option 1</option> <option>Option 2</option> </SelectField> <FieldError name="selectSingle" style={{ color: 'red' }} />
<SelectField multiple={true} name="selectMultiple" validation={{ required: true, validate: { matchesInitialValue: (value) => { let returnValue = [true] returnValue = value.map((element) => { if (element === 'Please select an option') return 'Select an Option' }) return returnValue[0] }, }, }} > <option>Please select an option</option> <option>Option 1</option> <option>Option 2</option> </SelectField> <FieldError name="selectMultiple" style={{ color: 'red' }} />

注意validate的返回值语义:返回true表示通过,返回字符串则视为该字符串为错误消息。上面的写法正是利用这一特性,把'Select an Option'作为校验失败时的提示文案。

类型转换

通常情况下<SelectField>返回字符串,但你可以用valueAs系列属性让返回值变成其他类型。典型场景是用下拉框选择数字型 ID:不使用valueAsNumber时返回字符串,开启后则返回数字(对应 GraphQL 的Int):

<SelectField name="select" validation={{ valueAsNumber: true }}> <option value={1}>Option 1</option> <option value={2}>Option 2</option> <option value={3}>Option 3</option> </SelectField>

如果用户选择了Option 3<Form>onSubmit收到的数据为:

{ select: 3, }

注意此场景下 Redwood 的emptyAsvalueAsNumber的交互:当二者同时出现时,Redwood 会在内部用setValueAs承担转换职责(见 coercion.ts),空值行为仍由emptyAs决定,未设emptyAs时空选返回NaN

<FieldError>:字段级错误消息

<FieldError>在具有相同name属性的字段存在校验错误时,渲染一个包含错误消息的<span>;否则什么都不渲染。

<FieldError name="name" className="error-message" /> <!-- 渲染为:<span class="error-message">name is required</span> -->

从 FieldError.tsx 可以看到,当校验错误未提供自定义message时,Redwood 会按错误类型给出默认文案:

错误类型默认消息
requiredis required
patternis not formatted correctly
minLengthis too short
maxLengthis too long
minis too low
maxis too high
validateis not valid

最终渲染的文本为`${name} ${DEFAULT_MESSAGES[type]}`(如name is required),若validation中提供了message则优先使用。FieldError在错误不存在时返回null,因此可以放心地在每个字段下方放置,不会产生多余 DOM。

测试与验证

@redwoodjs/forms的测试集中在 packages/forms/src/tests/form.test.tsx,覆盖了字段注册、校验、emptyAs空值行为、服务端错误注入等关键路径。如果你要验证自定义字段的行为是否符合预期,可以参考该测试文件中的写法,用@testing-library/react渲染表单并断言提交数据与错误样式。

小结与最佳实践

  • 优先使用命名输入字段<TextField><NumberField><DateField>等已针对类型预置了默认转换(InputComponents.tsx 批量生成),比手动设置<InputField type>更省心;
  • 重视空值语义:数据库关联字段(nameId结尾)为空时自动返回null;需要其他空值语义时用emptyAs,自定义转换时用setValueAs(优先级最高);
  • 善用 GraphQL 友好的转换valueAsBooleanvalueAsJSON让前端提交的数据类型与 GraphQL 服务端期望对齐,避免大量手写setValueAs
  • 服务端错误展示:在表单顶部放置<FormError error={error} />,配合useMutation返回的error对象;BAD_USER_INPUT类型错误会自动展示字段级消息,同时useErrorStyles会把服务端字段错误同步到对应输入框的错误样式(见 useErrorStyles.ts);
  • 需要 RHF 高级能力时:直接使用useForm并通过formMethods传入<Form>,或使用Controller集成受控组件库;
  • 封装自定义字段:非受控场景用useRegister+useErrorStyles,受控组件库场景用Controller+useErrorStyles

更多相关上下文可继续阅读 docs/docs/forms.md(当前版本)以及 Redwood 教程中关于表单的章节(docs/docs/tutorial),理解表单在真实 CRUD 页面中的完整用法。

  • 后端
  • 前端
  • Web框架
  • 开发工具

【免费下载链接】redwood

RedwoodGraphQL

项目地址:https://gitcode.com/gh_mirrors/re/redwood
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询