當React框架引入Redux以后,組件自己邏輯都不在出現(xiàn)了,但是那我們現(xiàn)在的代碼來說,對于表單的驗證和提交是很大的一塊.這個地方的邏輯也急需要轉(zhuǎn)移到Redux來統(tǒng)一指揮,保持UI組件的純潔性. 所以由組件Redux-form來完成這件事情.
從Medium看到這篇文章,翻譯來看看
Using Redux Form to handle user input
未見得是好文章,但是總要開個頭.
譯文開始
這篇文章是我學(xué)習構(gòu)建React APP的系列學(xué)習文章
大多數(shù)web app中處理用戶輸入是通過HTML的表單來完成.我們先假設(shè),用戶的輸入改變了application的state.在這個系列文章中,我將會討論一下怎么使用Redux來管理application的state.所以,如果能容易的的把HTML表單連接到Redux將會非常的好.
上面的這個需求正是redux-form包所做的工作.
坦白講,我不得不承認在是否使用redux-form的問題上廢話說的有點多了.首先,我感到很激動,因為發(fā)現(xiàn)了一個軟件包恰如其分的滿足了我的需求.接著我同事使用了redux-form和react-bootstrap,并且決定在我編寫代碼的時候把HTML表單輸入到Redux的時候好像是拴在了鏈條上不能動彈.經(jīng)過一段時間積累了一些經(jīng)驗以后.我意識到問題不是redux-form,而是我自身的問題.
TL;DR(太長了,不要讀了):如果你正在使用redux,我推薦你使用redux-form.(這一塊先不翻譯).
在你使用redux-form的時候兩塊內(nèi)容是比較重要的:
- reduxForm()裝飾器.這個裝飾器工作起來和react-redux的connect()函數(shù)很類似.使用redux-form包裝組件以后,功能才可以正常運轉(zhuǎn).通常情況下,包裝的組件包含有HTML元素form表單元素.我更愿意把這個元素叫做”form組件”.
- 字段和字段數(shù)組組件 添加字段和字段數(shù)組作為表單組件的后代.后代包括有HTML的輸入元素例如:<input>和<select>.
在定制組件中使用redux-form
不一定非要在redux-form中使用標準的HTML 輸入元素.你可以使用你自己的定制組件.
這一點是我剛開始犯糊涂的地方.我正在使用React-bootstrap,所以我已經(jīng)有了定制化的react-bootstrap模板-FromGroup,FormControl,Label,HelpBack等等.-所以我的所有的輸入項看起來一致性非常強.
起初,我試著把redux-form的字段作為react-bootstrap的FormControl的子組件.讓你少受一點痛苦的經(jīng)驗:如果你同時使用react-bootstrap和redux-form的話,redux-form字段組件在外面,react-bootstrap FormControl組件在內(nèi)部!
總體上看,我的渲染的組件樹看起來像是這樣:
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.
當然,這些元素有時是多次重復(fù)的,所以3-6在表單組件內(nèi)容部是可以多次重復(fù)的.
redux-form 字段組件
字段組件會傳遞幾個重要的props給定制化的組件:
- name 給字段組件添加的相同的name prop
- input 這是一個對象包括字段需要的props:name,onChange和其他的事件操作句柄和傳遞的value.value prop的傳遞把input變成了一個受控的組件.
- meta 這是一個對象包含有字段的狀態(tài)信息:是否被觸控,是否有臟數(shù)據(jù),驗證錯誤信息,等等.
字段也可以傳遞其他你想傳遞的props.這個方面在文檔中描述的很詳細.
reduxForm()裝飾器
reduxForm()裝飾器把一系列的事件操作句柄傳遞到你的form組件-最重要的是,hadleSubmit.通常情況下,你也可以設(shè)置你自己的表單提交方法屬性.
此外,直接可以傳遞自己的onSumbit函數(shù)作為porps.
嗯?犯糊涂了?
這個辦法是當一個表單被提交的時候-通過借助javascript點擊按鈕,觸控進入按鈕,或者其他的途徑-redux-form調(diào)用他自己的handleSubmint函數(shù).這個函數(shù)會執(zhí)行你已經(jīng)設(shè)定好的驗證方法.(譯注:好啊,這樣代碼組織起來很好看了).
handleSubmit函數(shù)調(diào)用的唯一途徑是onSubmit當前的表單值是有效的(譯注:如果是有數(shù)據(jù)驗證的設(shè)置,必須要通過驗證).
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
讓我們假設(shè)當你的表單提交的時候,會dispatch一個Redux action.把以上所有的內(nèi)容都考慮到,表單組件的代碼是這個樣子的:
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)
表單的數(shù)據(jù)在哪里存著呢?
在導(dǎo)入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.
在對象內(nèi)部有一系列的數(shù)據(jù),redux-form使用這些數(shù)據(jù)來追蹤你的表單的state:初始化和當前的字段的值,每個字段的驗證狀態(tài),字段是否被觸控過或者初始值是否被改變過(譯注:真的是需要好好研究一下這些state).
注意到form的reducer包含了所有字段的初始值和當前值.理解這一點非常的重要:redux-form connect你的的表單組件和字段值到他自己的reducer state-不是你的application中的state.
如果你想更新你自己reducers的其中一個state,你需要做的和我上面的代碼中一樣的事情:在你的onSubmit函數(shù)中,dispatch一個其他reducer(s)能夠操作的action.在很多例子中,你需要發(fā)送表單數(shù)據(jù)到API,因此actions可能是異步的(參見異步actions).
這么做違反了Redux的保持state的唯一性?技術(shù)層面上,或許是.但是我認為把application的state和redux-form的state分開還是很合情合理的:redux-form的reducer是一個臨時的State.一旦表單被驗證然后提交,數(shù)據(jù)就會變成”真的”,之后你可以在application的state中更新數(shù)據(jù).
在我的app中,onSubmit dispatch一個異步的操作請求API調(diào)用,只有異步action返回以后-也就是在數(shù)據(jù)被成功發(fā)送到server之后-所以我再dispatch一個SAVE_SUCCEEDED action,用來更新我自己的reducer state.
這個模式工作正常,我喜歡redux-form和我的application的state之間沒有直接聯(lián)系-我自己可以控制state什么時候怎么來更新,這個基于表單事件處理句柄里dispatch的action.如果我想把redux-form移走,操作也會讓你容易.
你不一定非要按著這個模式來做,但是這個模式是最直接的方法.Redux-form可以允許你定制form state的存儲.你的reducer可以直接監(jiān)聽redux-form的FORM_SUBMITTED的action后者其他的action.如果你想定制需要的方法,有很多途徑可以實現(xiàn).
使用redux-form來驗證數(shù)據(jù)
如果你閱讀了表單組建的代碼,你會看到我傳遞了兩個值到reduxForm():warn和error.這兩個地方我還沒有討論過.他們是用于驗證的函數(shù).
從error函數(shù)返回驗證錯誤的信息將會組織redux-form提交你的表單.也就是說,只要error函數(shù)返回有內(nèi)容,handleSubmit將不會調(diào)用onSubmit函數(shù).
與此不同的是,從warn函數(shù)返回驗證警告信息將不會阻止表單的提交.warn函數(shù)只會使得表單和字段元素中顯示警告信息(由reducForm()包裝的),表單還是可以提交的.
我正在構(gòu)建的application中使用了很多的warnings.我經(jīng)常需要讓用戶保存部分完成的工作到服務(wù)端.類似于有拼寫錯誤或者空主題的email草稿.
表單提交的值-是嵌套還是扁平化的
另一個在表單組件中需要注意的細節(jié)是,傳遞到字段組建的字段名: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等等(不管是扁平結(jié)構(gòu)或者是巢式結(jié)構(gòu),你都可以使用數(shù)組例如aliases來給字段組件取別名,后者直接使用索引).
你到底應(yīng)該使用扁平的還是巢式結(jié)構(gòu)?完全在于你的選擇,也可以混合使用兩者,只要覺得合適.但是要注意對象要遵守的規(guī)則:
- 傳遞到reducForm()的初始值的prop
- 傳遞給字段組件的名字(或者點路徑)
- 傳遞給onSubmit函數(shù)的值對象
- 傳遞給warn和erroe驗證函數(shù)的值對象-還有從這些函數(shù)中返回的對象.
更多關(guān)于驗證的考慮
這是最后一塊是異常處理.每個驗證函數(shù)為每個驗證失敗的字段返回一個包含warning/error消息的對象.當所有的字段通過驗證以后,返回一個空的{}對象.
在扁平的對象中,驗證錯誤的信息可能是:
{
side: 'The dark side of the Force is not allowed.'
}
在巢式結(jié)構(gòu)中,可能是:
{
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 驗證函數(shù)中返回的值必須和表單的值有同樣的表述,否則redux-form就不知道怎么把錯誤/警告信息和相應(yīng)的組件字段驗證狀態(tài)聯(lián)系起來.
使用大規(guī)模表單的性能
如果一個表單有很多字段,需要留意到性能問題.redux-form 5.x到6.x的API的變化主要就是考慮到大規(guī)模表單的性能改進問題.
即使在redux-form 6.x中我也看到一些字段的緩慢表現(xiàn)(開發(fā)階段).Redux-form創(chuàng)建被控制的表單輸入項,他也追蹤表單和每個字段的信息.每一次聚焦/改變/失去焦點,驗證狀態(tài)改變等等,都會dispatch action.結(jié)果是導(dǎo)致redux-form state的一些修改.如果你不太關(guān)心這個問題,這會導(dǎo)致整個表單經(jīng)常處于重新渲染中.
我能解決面臨的速度問題通過使用單純組件(意思是僅僅在頂層prop或者state的值發(fā)生改變的時候才重新渲染).在其他例子中通過實施shouldComponentUpdata()來限制更新的發(fā)生.這是在大規(guī)模React應(yīng)用匯總通常的做法-但是你可能需要在使用redux-form的時候盡早使用這個生命周期函數(shù).
如果你需要在字段的onChange時做一些事情,應(yīng)該要慎重考慮一下節(jié)流問題確保只有在用戶輸入停下來的時候再執(zhí)行相關(guān)的操作.
結(jié)論
Redux-form有點復(fù)雜.他們處理所有使用中的典型用例,但是需要你化一些時間去搞明白到底是怎么工作的.
你或許需要單純組件或者shouldComponentUpdate()來阻止組件的頻繁渲染問題,要在開發(fā)中盡早考慮這個問題.
我還沒有接觸過字段數(shù)組,異步(服務(wù)端)驗證,值的范式化,以及其他一些高級的用法.
綜合考慮這些問題,現(xiàn)在我已經(jīng)愛上了redux-form了.他幫助我搞定app中的表單問題,所以我強烈推薦他.
譯注:redux-form確實是有點難度,但是為了后續(xù)的工作開展,咬著牙也要把這一塊拿下.所以才考慮翻譯幾篇相關(guān)的文章.我感覺對于基本的概念還是能吃透的,但是有些細節(jié)問題可能理解有誤.先翻譯出來,后面再做更正吧.這個過程和跑馬拉松一樣,先跑上一回,看看到底是怎么一回事情.