- 后端
- 前端
- Web框架
- 开发工具
【免费下载链接】redwood
RedwoodGraphQL
Redwood 在@redwoodjs/forms包中提供了一系列表单辅助组件,它们是对 React Hook Form(RHF)的轻量封装,让你用声明式的 JSX 就能完成校验、错误样式、服务端错误展示与类型转换。本文以 Redwood 官方文档(docs/versioned_docs/version-5.x/forms.md)为骨架,结合packages/forms/src下的真实源码实现,系统讲解每个组件的用法、validation/emptyAs等核心 Props 的行为细节,以及如何通过useRegister、useErrorStyles和 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 原生提供valueAsDate、valueAsNumber,但 Redwood 因后端使用 GraphQL,额外增加了valueAsBoolean和valueAsJSON两个类型转换辅助项,加上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 独有的类型转换辅助项valueAsBoolean、valueAsJSON。对应源码类型为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钩子的选项 |
formMethods | useForm返回的函数集合。仅当你需要访问useForm返回的某个函数(如下例的reset)时才需要 |
onSubmit | 校验成功后调用的函数,入参为包含表单所有字段 name-value 对的对象 |
其他所有 Props 都会透传给其渲染的<form>标签。
<Form>的内部机制
从 Form.tsx 的实现可以看到,<Form>实际封装了三层:
- 内部调用
useForm(config)创建表单实例(若传入了formMethods则优先使用外部实例); - 将
errorProps 中 GraphQL 错误扩展里的字段消息(errorProps?.graphQLErrors?.[0]?.extensions?.properties?.messages)注入ServerErrorsContext,供useErrorStyles消费; - 通过
<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> ) }config与formMethods的区别在于: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)中的消息。
它还支持wrapperClassName、titleClassName、listClassName、listItemClassName及对应 Style Props,用于整体控制错误块的样式。
<Label>:可感知错误的标签
<Label>渲染 HTML<label>标签,并根据关联字段是否存在校验错误切换className和style。它既可以自闭合(此时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一致 |
errorClassName | 同name字段存在校验错误时使用的类名 |
errorStyle | 同name字段存在校验错误时使用的样式 |
实现上,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 |
valueAsDate和valueAsNumber是 React Hook Form 内置的、基于 HTML 标准的转换。但因为 Redwood 后端使用 GraphQL,表单提交的类型必须与 GraphQL 服务端期望一致。为避免用户大量手写setValueAs,Redwood 在 RHF 的valueAs系列之上额外提供了两个便捷项:
valueAsBooleanvalueAsJSON
源码层面,setCoercion(coercion.ts)负责整个转换流程的编排:当设置了valueAsJSON时,会把校验函数替换为JSONValidation(解析失败返回NaN时判为无效),并删掉valueAsJSON标记;当类型为date/datetime-local或显式设置valueAsNumber时也分别走对应分支;最终统一把转换逻辑包装成 RHF 的setValueAs函数。
空输入值的默认处理
Redwood 对空输入值提供了灵活的处理机制,合理的空值语义能让数据库关联字段的处理变得更简单。空字段行为由以下优先级规则决定:
- 若用户指定了
setValueAs,由该函数决定空字段行为; - 若设置了
emptyAsProp,则emptyAs决定空字段的值(取值见下文); - 若设置了
validation = { required: true },空字段返回null——同时 RHF 的required校验会生效,阻止空值表单提交; - 若字段是 Id 字段(
name以"Id"结尾),空字段返回null,这对大多数数据库关联字段是最合适的值;需要其他值时可使用emptyAs覆盖; - 若以上均不适用,空字段按字段类型采用默认值:
- DateFields →
null - NumberFields →
NaN - TextFields with
valueAsNumber→NaN - SelectFields with
valueAsNumber→NaN - SelectFields without
valueAsNumber→''(空字符串) - TextFields with
valueAsJSON→null - TextFields 及同类 →
''(空字符串)
- DateFields →
这套决策链在源码中有完整对应实现:coercion.ts 的注释详细记录了上述 5 条规则,getSetValueAsFn(coercion.ts)则按emptyAs、required、isId(即name.endsWith('Id'))组合出具体的转换函数,SET_VALUE_AS_FUNCTIONS表中为valueAsDate、valueAsJSON、valueAsNumber、valueAsString四种类型各自实现了emptyAsNull、emptyAsUndefined、emptyAsZero、emptyAsString、emptyAsNaN等变体。
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功能;同时若valueAsNumber与emptyAs同时使用,Redwood 会删除valueAsNumber,改由内部的setValueAs完成等价转换(coercion.ts),以保证emptyAs的优先级。
自定义输入字段(useRegister 与 useErrorStyles)
你可以通过 Redwood 提供的useRegister和useErrorStyles两个 Hook 创建与 Redwood 体系无缝集成的自定义字段:
useRegister:把字段注册进 React Hook Form,是 RHFregister的包装(useRegister.ts)。它会调用setCoercion注入默认转换,合并onBlur/onChange回调,并返回可直接展开到原生元素上的{ ref, onBlur, onChange, ... };若缺失name会抛出错误('name' prop must be provided)。useErrorStyles:为自定义字段建立错误样式。其实现(useErrorStyles.ts)会读取useFormContext的formState.errors和ServerErrorsContext中的服务端错误;若存在服务端错误,还会通过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 表单。下面是用primereact的ToggleButton封装成 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 多选。当multiple为true时,字段返回的值数组按选项在列表中的顺序排列,而不是用户选择的顺序:
<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 的emptyAs与valueAsNumber的交互:当二者同时出现时,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 会按错误类型给出默认文案:
| 错误类型 | 默认消息 |
|---|---|
required | is required |
pattern | is not formatted correctly |
minLength | is too short |
maxLength | is too long |
min | is too low |
max | is too high |
validate | is 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>更省心; - 重视空值语义:数据库关联字段(
name以Id结尾)为空时自动返回null;需要其他空值语义时用emptyAs,自定义转换时用setValueAs(优先级最高); - 善用 GraphQL 友好的转换:
valueAsBoolean、valueAsJSON让前端提交的数据类型与 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
相关推荐
Redwood Forms 完全指南:基于 React Hook Form 的声明式表单开发
Redwood Forms 完全指南:基于 React Hook Form 的声明式表单开发 Redwood 框架在 @redwoodjs/forms 中提供了
后端前端Web框架开发工具Redwood 表单体系完全指南:基于 `@redwoodjs/forms` 与 React Hook Form 的验证、错误处理与类型转换实战
Redwood 表单体系完全指南:基于 @redwoodjs/forms 与 React Hook Form 的验证、错误处理与类型转换实战 导读 @redwo
后端前端Web框架开发工具Redwood Forms 完全指南:基于 React Hook Form 的表单构建与校验体系
Redwood Forms 完全指南:基于 React Hook Form 的表单构建与校验体系 Redwood 框架通过 @redwoodjs/forms 包
后端前端Web框架开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考