乐趣区

React学习之Redux高阶运用

在 Redux 架构中,reducer 是一个纯函数,它的职责是根据 previousState 和 action 计算出新的 state。在复杂应用中,Redux 提供的 combineReducers 让我们可以把顶层的 reducer 拆分成多个小的 reducer,分别独立地操作 state 树的不同部分。而在一个应用中,很多小粒度的 reducer 往往有很多重复的逻辑,那么对于这些 reducer,如何抽取公共逻辑,减少代码冗余呢?这种情况下,使用高阶 reducer 是一种较好的解决方案

  1. 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,我们可以根据自己的项目结构来决定,例如 ${页面名称}_${模块名称}。只要能够保证全局唯一性,就可以写成一种前缀。

  1. reducer 增强

除了解决复用问题,高阶 reducer 的另一个重要作用就是对原始的 reducer 进行增强。redux-undo 就是典型的利用高阶 reducer 来增强 reducer 的例子,它主要作用是使任意 reducer 变成可以执行撤销和重做的全新 reducer。我们来看看它的核心代码实现:

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 进行封装:

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 处理。

退出移动版