React系列---Redux高階運用

參考資料:《深刻React技術棧》react


高階reducer

高階函數是指將函數做爲參數或返回值的函數,高階reducer就是指將reducer做爲參數或返回值的函數。

在Redux架構中,reducer是一個純函數,它的職責是根據previousState和action計算出新的state。在複雜應用中,Redux提供的combineReducers讓咱們能夠把頂層的reducer拆分紅多個小的reducer,分別獨立地操做state樹的不一樣部分。而在一個應用中,不少小粒度的reducer每每有不少重複的邏輯,那麼對於這些reducer,如何抽取公共邏輯,減小代碼冗餘呢?這種狀況下,使用高階reducer是一種較好的解決方案。redux

reducer複用

咱們將頂層的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的另外一個重要做用就是對原始的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:雙向綁定

  • 可以處理額外的action;
  • 可以維護更多的state;
  • 將不能處理的action委託給原始reducer處理。

Redux與表單

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減小建立表單的冗餘代碼

瞭解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完成表單異步驗證

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技術棧》

相關文章
相關標籤/搜索