當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確實是有點難度,但是為了后續的工作開展,咬著牙也要把這一塊拿下.所以才考慮翻譯幾篇相關的文章.我感覺對于基本的概念還是能吃透的,但是有些細節問題可能理解有誤.先翻譯出來,后面再做更正吧.這個過程和跑馬拉松一樣,先跑上一回,看看到底是怎么一回事情.