- 前端
- UI组件
【免费下载链接】redux-form
A Higher Order Component using react-redux to keep form state in a Redux store
FormSection是 redux-form 提供的一个轻量级布局组件,它通过为内部所有Field、Fields、FieldArray的字段名自动添加统一前缀,让开发者可以把大型表单拆解成可跨表单复用的子组件。读完本文,你将掌握FormSection的完整 API、底层sectionPrefix前缀机制、嵌套区块的字段命名规则,以及"订单表单买家/收货人信息复用"这类真实场景的落地写法。
一、FormSection 是什么
在 redux-form 中,表单状态始终保存在 Redux store 里,字段的路径由字段名决定。当表单越来越大时,把"地址"、"联系人"这类重复出现的区块抽成独立组件是常见的工程化手段。但直接抽组件有一个问题:每个区块的字段名必须手工带上前缀(如buyer.address.streetName),否则多个区块会在 store 里互相覆盖。
FormSection正是为了解决这一问题而设计的。官方文档(docs/api/FormSection.md)给出的定义是:
The
FormSectioncomponent makes it easy to split forms into smaller components that are reusable across multiple forms. It does this by prefixing the name ofField,FieldsandFieldArraychildren, at any depth, with the value specified in thenameprop.
即:它会把所有子级(任意深度)Field、Fields、FieldArray的名称统一加上name属性指定的前缀。也就是说,FormSection自己不渲染任何输入控件,它只是通过 React Context 向下传递一个"名称前缀",让子孙字段在注册、读写 store 时都带上这个前缀。
从源码看,FormSection的核心实现非常短小(src/FormSection.js):
class FormSection extends Component<PropsWithContext> { render() { const { _reduxForm, children, name, component, ...rest } = this.props if (React.isValidElement(children)) { return createElement(ReduxFormContext.Provider, { value: { ...this.props._reduxForm, sectionPrefix: prefixName(this.props, name) }, children }) } return createElement(ReduxFormContext.Provider, { value: { ...this.props._reduxForm, sectionPrefix: prefixName(this.props, name) }, children: createElement(component, { ...rest, children }) }) } }二、可传给 FormSection 的 Props
1.name : String(必填)
The name all child fields should be prefixed with.
所有子字段需要被加上的前缀名称。它是必填项,在propTypes中被声明为PropTypes.string.isRequired(见 src/FormSection.js)。
2.component : String | Component(可选)
If you give
FormSectionmore than one child element, it will be forced to create a component to wrap them with. You can specify what type of component you would like it to be (div,section,span). Defaults to'div'.
当FormSection有多个子元素时,必须创建一个包裹组件来容纳它们。你可以指定该包裹组件的类型:原生标签字符串(div、section、span)或任意 React 组件。默认值是'div',对应源码中的:
FormSection.defaultProps = { component: 'div' }重要细节:当FormSection只有一个子元素时,它不会包裹多余的div。这一点由测试用例验证(src/tests/FormSection.spec.js):渲染<FormSection name="foo"><Field name="bar" /></FormSection>后,页面中div标签数量为 0。这也是为什么上文源码中存在React.isValidElement(children)分支——单一 React 元素直接放进Provider,无需包装。
Note that any additional props (e.g. 'className', 'style') that you pass to
FormSectionwill be passed along to the wrapper component.
额外 props 透传:你传给FormSection的任何额外 props(如className、style)都会原样传递给包裹组件。注意name和component两个 props 会被消费掉,不会透传下去。测试用例对此有明确断言(src/tests/FormSection.spec.js):
<FormSection name="foo" component="section" className="form-section" style={{ fontWeight: 'bold' }} > <Field name="bar" component="input" /> <Field name="baz" component="input" /> </FormSection>断言结果为:className === 'form-section'、style.fontWeight === 'bold',而props.name与props.component均为 falsy(不会被透传到 DOM)。
三、工作原理:sectionPrefix 与 ReduxFormContext
FormSection能够自动加前缀,靠的是 redux-form 内部基于 React Context 的_reduxForm机制(src/ReduxFormContext.js):
export const ReduxFormContext = React.createContext(null)FormSection的渲染结果本质上是一个ReduxFormContext.Provider,它把从reduxForm()高阶组件拿到的_reduxForm复制一份,并覆写sectionPrefix字段。前缀的拼接逻辑在工具函数prefixName中(src/util/prefixName.js):
const formatName = ({ _reduxForm: { sectionPrefix } }, name) => sectionPrefix ? `${sectionPrefix}.${name}` : name也就是说,只要 context 中存在sectionPrefix,字段名就会变成前缀.字段名;否则原样返回。
这个前缀会沿着组件树向下传播,由下游组件消费:
Field:注册、取值、派发 change/blur 等动作时都会走prefixName(参见 src/createField.js,名称变化时还会自动注销旧名、注册新名);Fields:在 src/ConnectedFields.js 中从_reduxForm解构出sectionPrefix并处理字段名;FieldArray:在 src/ConnectedFieldArray.js 中把sectionPrefix传入createFieldArrayProps,后者会先剥离前缀再拼出fields[i]这类数组路径(src/createFieldArrayProps.js)。
需要留意的是:sectionPrefix的初始值是undefined(见 src/createReduxForm.js),只有被FormSection包住时才会变成具体的字符串前缀,因此未被包裹的普通表单完全不受影响。
四、完整示例:订单表单复用 Party 组件
官方文档给出的典型场景是订单表单:买家(buyer)和收货人(recipient)的信息字段完全相同,因此抽成一个Party组件;而Party内部又包含一组地址字段,地址也值得再抽成一个Address组件。完整代码如下:
//Address.js class Address extends React.Component { render() { return <div> <Field name="streetName" component="input" type="text"/> <Field name="number" component="input" type="text"/> <Field name="zipCode" component="input" type="text"/> </div> } } //Party.js class Party extends React.Component { render() { return <div> <Field name="givenName" component="input" type="text"/> <Field name="middleName" component="input" type="text"/> <Field name="surname" component="input" type="text"/> <FormSection name="address"> <Address/> </FormSection> </div> } } //OrderForm.js class OrderForm extends React.Component { render() { return <form onsubmit={...}> <FormSection name="buyer"> <Party/> </FormSection> <FormSection name="recipient"> <Party/> </FormSection> </form> } } //don't forget to connect OrderForm with reduxForm()外层表单用reduxForm()装饰,例如:
import { reduxForm } from 'redux-form' OrderForm = reduxForm({ form: 'order' })(OrderForm)Address、Party这样的区块组件可以是普通 class 组件或函数组件,因为FormSection的前缀能力来自 React Context,与组件自身是否连接 Redux 无关。
五、最终字段名与 store 结果结构
上述示例中字段的完整名称最终会变成buyer.address.streetName这样的点路径,对应 Redux store 中的嵌套结构:
{ buyer: { givenName: "xxx", middleName: "yyy", surname: "zzz", address: { streetName: undefined, number: "123", zipCode: "9090" } }, recipient: { givenName: "aaa", middleName: "bbb", surname: "ccc", address: { streetName: "foo", number: "4123", zipCode: "78320" } } }这套"字段名即 store 路径"的约定贯穿 redux-form 的取值、校验、错误上报等所有环节——getFormValues、getFormSyncErrors等 selector(参见 src/selectors 与 docs/api/Selectors.md)返回的都是这种嵌套结构,因此FormSection拆出的区块与整体表单的 Redux 状态天然一致。
六、嵌套 FormSection:前缀自动拼接
FormSection支持任意深度嵌套,前缀会自动逐层拼接。测试用例(src/tests/FormSection.spec.js)验证了嵌套场景:
<FormSection name="deep"> <FormSection name="foo"> <Field name="bar" component={input} /> </FormSection> </FormSection>最终字段的input.name为deep.foo.bar,并且能从 store 正确读取到deep.foo.bar路径下的值。这与文档示例中Party内嵌Address(buyer.address.streetName)的机制完全相同。
七、进阶技巧:继承 FormSection 固化默认前缀
对于Address这类很少改变区块名的组件,官方文档建议直接继承FormSection,并设置默认name,这样在使用处无需再写<FormSection name="address">:
class Address extends FormSection { //ES2015 syntax with babel transform-class-properties static defaultProps = { name: 'address' } render() { return ( <div> <Field name="streetName" component="input" type="text" /> <Field name="number" component="input" type="text" /> <Field name="zipCode" component="input" type="text" /> </div> ) } } //Regular syntax: /* Address.defaultProps = { name: "address" } */注意这里使用了static defaultProps(需babel-plugin-transform-class-properties支持)或等价的Address.defaultProps = ...写法。由于defaultProps的优先级低于显式传入的 props,调用处仍可用<Address name="shippingAddress" />覆盖默认前缀,实现"默认地址、可覆盖"的灵活复用。
八、使用注意事项与边界行为
结合源码与测试(src/tests/FormSection.spec.js),使用FormSection时有几点需要注意:
必须位于 reduxForm() 装饰的表单内部。
FormSection的构造函数会检查props._reduxForm,否则抛出'FormSection must be inside a component decorated with reduxForm()'(src/FormSection.js),测试同样断言了该行为(src/tests/FormSection.spec.js)。component prop 必须是合法组件。它经由
validateComponentProp校验(src/util/validateComponentProp.js),传入普通对象等非法值会在渲染时报Element type is invalid错误(src/tests/FormSection.spec.js)。单子元素不产生多余包裹层。如上文所述,只有一个子元素时
FormSection不会包一层div,这对输出干净 DOM 很有帮助。对
Field、Fields、FieldArray三类组件全部生效,且覆盖任意嵌套深度;校验、警告、异步错误等字段元数据同样以带前缀的字段名存储,测试中对registeredFields的断言(如'foo.bar[0]')即为佐证。Immutable 结构同样支持。测试同时以 plain 对象与 immutable 结构两套实现运行(src/tests/FormSection.spec.js),配合 src/immutable 目录下的等价实现使用即可。
九、小结
FormSection通过一句name前缀约定,把"可复用的表单区块"从理想变成了开箱即用的能力:对外它是布局组件,对内它是sectionPrefix的 Context 提供者。掌握它之后,无论是订单表单里的买家/收货人,还是大型后台系统中反复出现的地址、联系信息模块,都可以安全地抽成独立组件,并在任意多个表单中复用,而无需手工拼接字段路径。
- 前端
- UI组件
【免费下载链接】redux-form
A Higher Order Component using react-redux to keep form state in a Redux store
相关推荐
TanStack Form 的 Angular 表单组合指南:用 `TanStackAppField` 与 `tanstack-with-form` 拆分大型表单
TanStack Form 的 Angular 表单组合指南:用 TanStackAppField 与 tanstack with form 拆分大型表单 导读
前端UI组件portless 状态目录架构解析:~/.portless 文件布局与 sudo 路由共享指南
portless 状态目录架构解析:~/.portless 文件布局与 sudo 路由共享指南 portless 状态目录( ~/.portless )是这款本
开发工具CLI如何快速配置eslint-config-love:从ECMAScript Modules到CommonJS的终极指南
如何快速配置eslint config love:从ECMAScript Modules到CommonJS的终极指南 eslint config love是一款
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考