參考資料:《深刻React技術棧》react
高階函數是指將函數做爲參數或返回值的函數,高階reducer就是指將reducer做爲參數或返回值的函數。
在Redux架構中,reducer是一個純函數,它的職責是根據previousState和action計算出新的state。在複雜應用中,Redux提供的combineReducers讓咱們能夠把頂層的reducer拆分紅多個小的reducer,分別獨立地操做state樹的不一樣部分。而在一個應用中,不少小粒度的reducer每每有不少重複的邏輯,那麼對於這些reducer,如何抽取公共邏輯,減小代碼冗餘呢?這種狀況下,使用高階reducer是一種較好的解決方案。redux
咱們將頂層的reduce拆分紅多個小的reducer,確定會碰到reducer複用問題。例若有A和B兩個模塊,它們的UI部分類似,此時能夠經過配置不一樣的props來區別它們。那麼這種狀況下,A和B模塊能不能共用一個reducer呢?答案是否認的。咱們先來看一個簡單reducer:架構
const LOAD_DATA = 'LOAD_DATA'; const initialState = { ... }; function loadData() { return { type: LOAD_DATA, ... }; } function reducer(state = initialState, action) { switch(action.type) { case LOAD_DATA: return { ...state, data: action.payload }; default: return state; } }
若是咱們將這個reducer綁定到A和B兩個不一樣模塊,形成的問題將會是,當A模塊調用loadData來分發相應的action時,A和B的reducer都會處理這個action,而後A和B的內容就徹底一致了。框架
這裏咱們必需意識到,在一個應用中,不一樣模塊間的actionType必須是全局惟一的。異步
所以,要解決actionType惟一的問題,還有一個方法就是經過添加前綴的方式來作到:函數
function generateReducer(prefix, state) { const LOAD_DATA = prefix + 'LOAD_DATA'; const initialState = { ...state, ...}; return function reducer(state = initialState, action) { switch(action.type) { case LOAD_DATA: return { ...state, data: action.payload }; default: return state; } } }
這樣只要A和B模塊分別調用generateReducer來生成相應的reducer,就能解決reducer複用的問題了。而對於prefix,咱們能夠根據本身的項目結構來決定,例如${頁面名稱}_${模塊名稱}。只要可以保證全局惟一性,就能夠寫成一種前綴。工具
除了解決複用問題,高階reducer的另外一個重要做用就是對原始的reducer進行加強。redux-undo就是典型的利用高階reducer來加強reducer的例子,它主要做用是使任意reducer變成能夠執行撤銷和重作的全新reducer。咱們來看看它的核心代碼實現:this
function undoable(reducer) { const initialState = { // 記錄過去的state past: [], // 以一個空的action調用reducer來產生當前值的初始值 present: reducer(undefined, {}), // 記錄後續的state future: [] }; return function(state = initialState, action) { const { past, present, future } = state; switch(action.type) { case '@@redux-undo/UNDO': const previous = past[past.length - 1]; const newPast = past.slice(0, past.length - 1); return { past: newPast, present: previous, future: [ present, ...future ] }; case '@@redux-undo/REDO': const next = future[0]; const newFuture = future.slice(1); return { past: [ ...past, present ], present: next, future: newFuture }; default: // 將其餘action委託給原始的reducer處理 const newPresent = reducer(present, action); if(present === newPresent) { return state; } return { past: [ ...past, present ], present: newPresent, future: [] }; } }; }
有了這高階reducer,就能夠對任意一個reducer進行封裝:spa
import { createStore } from 'redux'; function todos(state = [], action) { switch(action.type) { case: 'ADD_TODO': // ... } } const undoableTodos = undoable(todos); const store = createStore(undoableTodos); store.dispatch({ type: 'ADD_TODO', text: 'Use Redux' }); store.dispatch({ type: 'ADD_TODO', text: 'Implement Undo' }); store.dispatch({ type: '@@redux-undo/UNDO' });
查看高階reducer undoable的實現代碼能夠發現,高階reducer主要經過下面3點來加強reducer:雙向綁定
React單向綁定的特性極大地提高了應用的執行效率,可是相比於簡單易用的雙向綁定,單向綁定在處理表單等交互的時候着實有些力不從心。具體到React應用中,單向綁定意味着你須要手動給每一個表單控件提供onChange回調函數,同時須要將它們的狀態初始化在this.state中。不只如此,一個體驗友好的表單還須要有明確的錯誤狀態和錯誤信息,甚至某些輸入項還須要異步校驗功能。也就是說,表單裏的一個有效字段至少須要2~3個本地狀態。
在Angular.js中,表單相關的問題在框架層面已經獲得了很好的解決。那麼,對於React+Redux應用,有沒有什麼好的方案呢?
下面咱們從兩個層面來解答這個問題:對於簡單的表單應用,爲了減小重複冗餘的代碼,可使用redux-form-utils這個工具庫,它能利用高階組件的特性爲表單的每一個字段提供value和onChange等必須值,而無需你手動建立;對於複雜的表單,則能夠利用redux-form。雖然一樣基於高階組件的原理,但若是說redux-form-utils是一把水果刀的話,那麼redux-form就是一把多功能的瑞士軍刀。除了提供表單必須的字段外,redux-form還能實現表單同步驗證、異步驗證甚至嵌套表單等複雜功能。
瞭解redux-form-utils以前,先來看看如何使用原生React處理表單:
import React, { Component } from 'react'; class Form extends Component { constructor(props) { super(props); this.handleChangeAddress = this.handleChangeAddress.bind(this); this.handleChangeGender = this.handleChangeGender.bind(this); this.state = { name: '', address: '', gender: '' }; } handleChangeName(e) { this.setState({ name: e.target.value }); } handleChangeAddress(e) { this.setState({ address: e.target.value }); } handleChangeGender(e) { this.setState({ gender: e.target.value }); } render() { const { name, address, gender } = this.state; return ( <form className="form"> <input name="name" value={name} onChange={this.handleChangeName} /> <input name="address" value={address} onChange={this.handleChangeAddress} /> <select name="gender" value={gender} onChange={this.handleChangeGender}> <option value="male" /> <option value="female" /> </select> </form> ); }; }
能夠看到,雖然咱們的表單裏只有3個字段,可是已經有很是多的冗餘代碼。若是還須要加上驗證等功能,那麼這個表單對應的處理代碼將會更加膨脹。
仔細分析表單的代碼實現,咱們發現幾乎全部的onChange處理器邏輯都很相似,只是須要改變表單字段便可。對於某些複雜的輸入控件,好比本身封裝了一個TimePicker組件,也許回調名稱不是onChange,而是onSelect。一樣,onSelect回調裏提供的參數也許並非React的合成事件,而是一個具體的值。經過分析表單控件可能的輸入和輸出,咱們將經過使用redux-form-utils減小Redux處理表單應用時的冗餘代碼:
// components/MyForm.js import React, { Component } from 'react'; import { createForm } from 'redux-form-utils'; @createForm({ form: 'my-form', fields: ['name', 'address', 'gender'] }) class Form extends Component { render(){ const { name, address, gender } = this.props.fields; return ( <form className="form"> <input name="name" value={...name} /> <input name="address" value={...address} /> <select {...gender}> <option value="male" /> <option value="female" /> </select> </form> ); } }
能夠看到,實現一樣功能的表單,代碼量減小了近一半以上。
redux-form-utils提供了兩個方便的工具函數---createForm(config)和bindRedux(config),前者能夠看成decorate使用,傳入表單的配置,自動爲被裝飾的組件添加表單相關的props;然後者能夠生成與Redux應用相關的reducer、initialState和actionCreator等。
下面先看看如何在reducer裏整合redux-form-utils:
// reducer/MyForm.js import { bindRedux } from 'redux-form-utils'; const { state: formState, reducer: formReducer } = bindRedux({ form: 'my-form', fields: ['name', 'address', 'gender'], }); const initialState = { foo: 1, bar: 2, ...formState }; function myReducer(state = initialState, action) { switch(action.type) { case 'MY_ACTION': { // ... } default: return formReducer(state, action); } }
咱們把一樣的配置傳給bindRedux方法,並得到這個表單對應的reducer和初始狀態formState,並將這些內容整合在reducer中。
完成createForm和bindRedux這兩個函數後,一個基於Redux的表單應用就完成了。爲了後續修改表單更加靈活,建議將配置文件單獨保存,並分別在組件和reducer中引入對應的配置文件。
redux-form-utils爲咱們提供了實現表單最基本的功能,可是爲了填寫表單的體驗更加友好,在把數據提交到服務端以前,咱們應該作一些基本的表單校驗,好比填寫字段不能爲空等。要實現校驗等複雜的表單功能,須要用到redux-form。
在使用和配置方面,redux-form與redux-form-utils沒有太多的差別,惟一不一樣的是redux-form須要在Redux應用的state樹中掛載一個獨立的節點。這意味着,全部使用redux-form建立的表單中的字段都會在一個固定的位置,如state.form.myForm或state.form.myOtherForm均掛載在state.form下:
import { createStore, combineReducers } from 'redux'; import { reducer as formReducer } from 'redux-form'; const reducers = { // 其餘的reducer... // 全部表單相關的reducer掛載在form下 form: formReducer }; const reducer = combineReducers(reducers); const store = createStore(reducer);
完成了基本的配置後,如今看看redux-form如何幫咱們完成表單驗證功能:
import React, { Component } from 'react'; import { reduxForm } from 'redux-form'; function validate(values) { if(values.name == null || values.name === '') { return { name: '請填寫名稱' }; } } @reduxForm({ form: 'my-form', fields: ['name', 'address', 'gender'], validate }); class Form extends Component { render(){ const { name, address, gender } = this.props.fields; return ( <form className="form"> <input name="name" value={...name} /> { name.error && <span>{name.error}</span> } <input name="address" value={...address} /> <select {...gender}> <option value="male" /> <option value="female" /> </select> <button type="submit">提交</button> </form> ); } }
在上面的表單中,咱們在提交時對name字段作了非空驗證,而在Form組件的render方法中,同時添加了顯示相應錯誤的邏輯。觸發驗證、從新渲染、表單純潔性判斷等過程,均被redux-form進行了封裝,對使用者透明。
能夠看到,使用redux-form校驗表單十分簡單易用,從很大程度上填補了Redux應用在框架層面處理表單應用的不足。
參考資料:《深刻React技術棧》