当React框架引入Redux以后,组件自己逻辑都不在出现了,但是那我们现在的代码来说,对于表单的验证和提交是很大的一块.这个地方的逻辑也急需要转移到Redux来统一指挥,保持UI组件的纯洁性. 所以由组件Redux-form来完成这件事情.
从Medium看到这篇文章,翻译来看看
Using Redux Form to handle user input
未见得是好文章,但是总要开个头.
译文开始
这篇文章是我学习构建React APP的系列学习文章
大多数web app中处理用户输入是通过HTML的表单来完成.我们先假设,用户的输入改变了application的state.在这个系列文章中,我将会讨论一下怎么使用Redux来管理application的state.所以,如果能容易的的把HTML表单连接到Redux将会非常的好.
上面的这个需求正是redux-form包所做的工作.
坦白讲,我不得不承认在是否使用redux-form的问题上废话说的有点多了.首先,我感到很激动,因为发现了一个软件包恰如其分的满足了我的需求.接着我同事使用了redux-form和react-bootstrap,并且决定在我编写代码的时候把HTML表单输入到Redux的时候好像是拴在了链条上不能动弹.经过一段时间积累了一些经验以后.我意识到问题不是redux-form,而是我自身的问题.
TL;DR(太长了,不要读了):如果你正在使用redux,我推荐你使用redux-form.(这一块先不翻译).
在你使用redux-form的时候两块内容是比较重要的:
- reduxForm()装饰器.这个装饰器工作起来和react-redux的connect()函数很类似.使用redux-form包装组件以后,功能才可以正常运转.通常情况下,包装的组件包含有HTML元素form表单元素.我更愿意把这个元素叫做”form组件”.
- 字段和字段数组组件 添加字段和字段数组作为表单组件的后代.后代包括有HTML的输入元素例如:<input>和<select>.
在定制组件中使用redux-form
不一定非要在redux-form中使用标准的HTML 输入元素.你可以使用你自己的定制组件.
这一点是我刚开始犯糊涂的地方.我正在使用React-bootstrap,所以我已经有了定制化的react-bootstrap模板-FromGroup,FormControl,Label,HelpBack等等.-所以我的所有的输入项看起来一致性非常强.
起初,我试着把redux-form的字段作为react-bootstrap的FormControl的子组件.让你少受一点痛苦的经验:如果你同时使用react-bootstrap和redux-form的话,redux-form字段组件在外面,react-bootstrap FormControl组件在内部!
总体上看,我的渲染的组件树看起来像是这样:
1. reduxForm() wrapper component from redux-form.
2. My form component (with HTML <form> element).
3. Field component from redux-form
4. My component with react-bootstrap boilerplate.
5. FormControl component from react-bootstrap.
6. HTML <input> element.
当然,这些元素有时是多次重复的,所以3-6在表单组件内容部是可以多次重复的.
redux-form 字段组件
字段组件会传递几个重要的props给定制化的组件:
- name 给字段组件添加的相同的name prop
- input 这是一个对象包括字段需要的props:name,onChange和其他的事件操作句柄和传递的value.value prop的传递把input变成了一个受控的组件.
- meta 这是一个对象包含有字段的状态信息:是否被触控,是否有脏数据,验证错误信息,等等.
字段也可以传递其他你想传递的props.这个方面在文档中描述的很详细.
reduxForm()装饰器
reduxForm()装饰器把一系列的事件操作句柄传递到你的form组件-最重要的是,hadleSubmit.通常情况下,你也可以设置你自己的表单提交方法属性.
此外,直接可以传递自己的onSumbit函数作为porps.
嗯?犯糊涂了?
这个办法是当一个表单被提交的时候-通过借助javascript点击按钮,触控进入按钮,或者其他的途径-redux-form调用他自己的handleSubmint函数.这个函数会执行你已经设定好的验证方法.(译注:好啊,这样代码组织起来很好看了).
handleSubmit函数调用的唯一途径是onSubmit当前的表单值是有效的(译注:如果是有数据验证的设置,必须要通过验证).
React's form onSubmit calls:
redux-form’s handleSubmit, which (if the form is valid) calls:
the function you pass in as a prop named onSubmit
让我们假设当你的表单提交的时候,会dispatch一个Redux action.把以上所有的内容都考虑到,表单组件的代码是这个样子的:
class MyForm extends React.Component {
// this.props.handleSubmit is created by reduxForm()
// if the form is valid, it will call this.props.onSubmit,
// which I added below in the connect() function.
const { handleSubmit } = this.props
render() {
<form onSubmit={handleSubmit}>
<Field name='user.email' component='input' type='email' />
<Field name='user.name' component='input' />
...
<input type='submit' value='Save' />
</form>
}
}
// Your component is wrapped by redux-form
// with the configuration you specify
const myReduxForm = reduxForm({
form: ‘myFormName’, // required by reduxForm()
warn: (values, props) => { ... }, // optional
error: (values, props) => { ... } // optional
})(MyForm)
// Your redux-form-wrapped component is wrapped by react-redux.
export default connect(
state => ({
// optional. grab values to fill the form from somewhere.
initialValues: state.foo.bar
}),
dispatch => ({
// reduxForm() expects the component to have an onSubmit
// prop. You could also pass this from a parent component.
// I want to dispatch a redux action.
onSubmit: data => dispatch(myActionToDoStuff(data))
})
)(myReduxForm)
表单的数据在哪里存着呢?
在导入redux-form时,额外需要配置的一块是reducer,在reducer中要做的是:
import { reducer as ‘formReducer’ } from ‘redux-form’
...
export default combineReducers({
// other reducers,
form: formReducer // must be named 'form'
})
当你的组件加载(mounted)的时候,表单reducer的state将会有一个顶级的值,这个值和你传递到reduxForm()的名字一样.针对上面的例子,表单的state位于对象的state.form.myFormName.
在对象内部有一系列的数据,redux-form使用这些数据来追踪你的表单的state:初始化和当前的字段的值,每个字段的验证状态,字段是否被触控过或者初始值是否被改变过(译注:真的是需要好好研究一下这些state).
注意到form的reducer包含了所有字段的初始值和当前值.理解这一点非常的重要:redux-form connect你的的表单组件和字段值到他自己的reducer state-不是你的application中的state.
如果你想更新你自己reducers的其中一个state,你需要做的和我上面的代码中一样的事情:在你的onSubmit函数中,dispatch一个其他reducer(s)能够操作的action.在很多例子中,你需要发送表单数据到API,因此actions可能是异步的(参见异步actions).
这么做违反了Redux的保持state的唯一性?技术层面上,或许是.但是我认为把application的state和redux-form的state分开还是很合情合理的:redux-form的reducer是一个临时的State.一旦表单被验证然后提交,数据就会变成”真的”,之后你可以在application的state中更新数据.
在我的app中,onSubmit dispatch一个异步的操作请求API调用,只有异步action返回以后-也就是在数据被成功发送到server之后-所以我再dispatch一个SAVE_SUCCEEDED action,用来更新我自己的reducer state.
这个模式工作正常,我喜欢redux-form和我的application的state之间没有直接联系-我自己可以控制state什么时候怎么来更新,这个基于表单事件处理句柄里dispatch的action.如果我想把redux-form移走,操作也会让你容易.
你不一定非要按着这个模式来做,但是这个模式是最直接的方法.Redux-form可以允许你定制form state的存储.你的reducer可以直接监听redux-form的FORM_SUBMITTED的action后者其他的action.如果你想定制需要的方法,有很多途径可以实现.
使用redux-form来验证数据
如果你阅读了表单组建的代码,你会看到我传递了两个值到reduxForm():warn和error.这两个地方我还没有讨论过.他们是用于验证的函数.
从error函数返回验证错误的信息将会组织redux-form提交你的表单.也就是说,只要error函数返回有内容,handleSubmit将不会调用onSubmit函数.
与此不同的是,从warn函数返回验证警告信息将不会阻止表单的提交.warn函数只会使得表单和字段元素中显示警告信息(由reducForm()包装的),表单还是可以提交的.
我正在构建的application中使用了很多的warnings.我经常需要让用户保存部分完成的工作到服务端.类似于有拼写错误或者空主题的email草稿.
表单提交的值-是嵌套还是扁平化的
另一个在表单组件中需要注意的细节是,传递到字段组建的字段名:user.email和user.name.
在redux-form中,表单的初始值和当前值是分隔开的对象.你可以使用扁平对象或者是嵌套的对象,扁平对象像这样:
{
email: 'askywalker@deathstar.com',
name: 'anakin'
side: 'dark',
aliases: ['darth vader', 'sith lord'],
}
所有的值都在对象属性的顶层.或者可以使用嵌套巢式对象像这样:
{
user: {
email: 'askywalker@deathstar.com',
name: 'anakin',
side: 'dark',
aliases: ['darth vader', 'sith lord'],
children: {
luke: {
name: 'luke',
planet: 'tatooine'
},
leia: {
name: 'leia',
planet: 'alderan'
}
}
},
}
在扁平的实例中,你的字段的字段名可能是email和name.在巢式实例中,你的字段名将会使用点路径表示每个字段值: user.name, user.email, user.childrn.leia.planet等等(不管是扁平结构或者是巢式结构,你都可以使用数组例如aliases来给字段组件取别名,后者直接使用索引).
你到底应该使用扁平的还是巢式结构?完全在于你的选择,也可以混合使用两者,只要觉得合适.但是要注意对象要遵守的规则:
- 传递到reducForm()的初始值的prop
- 传递给字段组件的名字(或者点路径)
- 传递给onSubmit函数的值对象
- 传递给warn和erroe验证函数的值对象-还有从这些函数中返回的对象.
更多关于验证的考虑
这是最后一块是异常处理.每个验证函数为每个验证失败的字段返回一个包含warning/error消息的对象.当所有的字段通过验证以后,返回一个空的{}对象.
在扁平的对象中,验证错误的信息可能是:
{
side: 'The dark side of the Force is not allowed.'
}
在巢式结构中,可能是:
{
user: {
side: 'The dark side of the Force is not allowed.'
children: {
leia: {
planet: '"alderan" is not a planet. Please check the
spelling and try again'
}
}
}
}
从error和warn 验证函数中返回的值必须和表单的值有同样的表述,否则redux-form就不知道怎么把错误/警告信息和相应的组件字段验证状态联系起来.
使用大规模表单的性能
如果一个表单有很多字段,需要留意到性能问题.redux-form 5.x到6.x的API的变化主要就是考虑到大规模表单的性能改进问题.
即使在redux-form 6.x中我也看到一些字段的缓慢表现(开发阶段).Redux-form创建被控制的表单输入项,他也追踪表单和每个字段的信息.每一次聚焦/改变/失去焦点,验证状态改变等等,都会dispatch action.结果是导致redux-form state的一些修改.如果你不太关心这个问题,这会导致整个表单经常处于重新渲染中.
我能解决面临的速度问题通过使用单纯组件(意思是仅仅在顶层prop或者state的值发生改变的时候才重新渲染).在其他例子中通过实施shouldComponentUpdata()来限制更新的发生.这是在大规模React应用汇总通常的做法-但是你可能需要在使用redux-form的时候尽早使用这个生命周期函数.
如果你需要在字段的onChange时做一些事情,应该要慎重考虑一下节流问题确保只有在用户输入停下来的时候再执行相关的操作.
结论
Redux-form有点复杂.他们处理所有使用中的典型用例,但是需要你化一些时间去搞明白到底是怎么工作的.
你或许需要单纯组件或者shouldComponentUpdate()来阻止组件的频繁渲染问题,要在开发中尽早考虑这个问题.
我还没有接触过字段数组,异步(服务端)验证,值的范式化,以及其他一些高级的用法.
综合考虑这些问题,现在我已经爱上了redux-form了.他帮助我搞定app中的表单问题,所以我强烈推荐他.
译注:redux-form确实是有点难度,但是为了后续的工作开展,咬着牙也要把这一块拿下.所以才考虑翻译几篇相关的文章.我感觉对于基本的概念还是能吃透的,但是有些细节问题可能理解有误.先翻译出来,后面再做更正吧.这个过程和跑马拉松一样,先跑上一回,看看到底是怎么一回事情.