1. Redux中间件 #

Redux 中间件提供了一个第三方扩展点,让你可以在 dispatch action 到达 reducer 之前,对这个 action 进行额外的操作。这些操作可能包括日志记录、创建延迟 action、处理异步逻辑,或者在某些情况下,条件性地阻止 action 被 dispatch。

中间件的功能通过 Redux 的 applyMiddleware 函数来启用,该函数可以在创建 store 的时候使用。。

redux-saga-flow2

2. 基础项目 #

2.1 src\index.js #

src\index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import {Provider} from 'react-redux';
import Counter from './components/Counter';
import store from './store';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <Provider store={store}>
        <Counter />
    </Provider>
);

2.2 Counter.js #

src\components\Counter.js

import React from 'react';
import {connect} from 'react-redux';
import actions from '../store/actions/counter';
function Counter(props) {
    const {number,add,minus} = props;
    return (
        <div>
            <p>{number}</p>
            <button onClick={add}>+</button>
            <button onClick={minus}>-</button>
        </div>
    );
}

export default connect(
    state => state.counter,
    actions
)(Counter);

2.3 store\index.js #

src\store\index.js

import { legacy_createStore as createStore, applyMiddleware } from 'redux';
import rootReducer from './reducers';
const store = createStore(rootReducer);
export default store;

2.4 action-types.js #

src\store\action-types.js

export const ADD = 'ADD';
export const MINUS = 'MINUS';

2.5 counter.js #

src\store\reducers\counter.js

import * as actionTypes from '../action-types';
let initialState = { number: 0 }
function counter(state = initialState, action) {
    switch (action.type) {
        case actionTypes.ADD:
            return { number: state.number + 1 };
        case actionTypes.MINUS:
            return { number: state.number - 1 };
        default:
            return state;
    }
}
export default counter;

2.6 reducers\index.js #

src\store\reducers\index.js

import { combineReducers } from "redux";
import counter from "./counter";
let rootReducer = combineReducers({
    counter
});
export default rootReducer;

2.7 counter.js #

src\store\actions\counter.js

import * as types from '../action-types';
const actions = {
    add() {
        return {type: types.ADD};
    }
    ,
    minus() {
        return {type:types.MINUS};
    }
};
export default actions;

3. redux-logger #

redux-logger 是一个 Redux 中间件,它在控制台记录了关于 actions 和 store 的信息,对于调试 Redux 应用非常有用。它记录的信息包括每个 action 的类型和数据,以及每次 action 被处理后新的 state。

3.1 store\index.js #

src\store\index.js

import { legacy_createStore as createStore, applyMiddleware } from 'redux';
+import logger from 'redux-logger';
import rootReducer from './reducers';
+const store = applyMiddleware(logger)(createStore)(rootReducer);
export default store;

4. redux-thunk #

redux-thunk 是一个 Redux 的中间件,允许我们写返回函数的 action creator,而不仅仅是对象。这使得我们可以在 action creator 中执行异步操作,如 API 请求等。

4.1 store\index.js #

src\store\index.js

import { legacy_createStore as createStore, applyMiddleware } from 'redux';
import logger from 'redux-logger';
+import thunk from 'redux-thunk';
import rootReducer from './reducers';
+const store = applyMiddleware(thunk,logger)(createStore)(rootReducer);
export default store;

4.2 counter.js #

src\store\actions\counter.js

import * as types from '../action-types';
const actions = {
    add() {
        return { type: types.ADD };
    },
+   thunkAdd() {
+       return function (dispatch) {
+           setTimeout(function () {
+               dispatch({ type: types.ADD });
+           }, 2000);
+       }
+   },
    minus() {
        return { type: types.MINUS };
    }
};
export default actions;

4.3 Counter.js #

src\components\Counter.js

import React from 'react';
+import {connect} from 'react-redux';
import actions from '../store/actions/counter';
function Counter(props) {
    const {number,add,minus,thunkAdd} = props;
    return (
        <div>
            <p>{number}</p>
            <button onClick={add}>+</button>
+            <button onClick={thunkAdd}>thunkAdd</button>
            <button onClick={minus}>-</button>
        </div>
    );
}
export default connect(
    state => state.counter,
    actions
)(Counter);

5. redux-promise #

redux-promise 是一个 Redux 的中间件,用于处理 action 中携带的 Promise 对象。当你 dispatch 一个 action,如果这个 action 的 payload 属性是一个 Promise,redux-promise 会等待这个 Promise 结束。如果 Promise resolve,redux-promise 会 dispatch 一个新的 action,并使用 Promise 的结果作为 payload。如果 Promise reject,redux-promise 会 dispatch 一个新的 action,并将错误作为 payload,并且为这个 action 添加一个 error 属性。

5.1 store\index.js #

src\store\index.js

import { legacy_createStore as createStore, applyMiddleware } from 'redux';
import logger from 'redux-logger';
import thunk from 'redux-thunk';
+import promise from 'redux-promise';
import rootReducer from './reducers';
+const store = applyMiddleware(thunk,promise,logger)(createStore)(rootReducer);
export default store;

5.2 counter.js #

src\store\actions\counter.js

import * as types from '../action-types';
const actions = {
    add() {
        return { type: types.ADD };
    },
    thunkAdd() {
        return function (dispatch) {
            setTimeout(function () {
                dispatch({ type: types.ADD });
            }, 2000);
        }
    },
+   promiseAdd() {
+       return new Promise((resolve, reject) => {
+           setTimeout(() => {
+               resolve({ type: types.ADD });
+           }, 1000);
+       });
+   },
+   payloadPromiseAdd() {
+       return {
+           type: types.ADD,
+           payload: new Promise((resolve, reject) => {
+               setTimeout(() => {
+                   const rand = Math.random();
+                   if ( rand> .5) {
+                       resolve(rand);
+                   } else {
+                       reject(rand);
+                   }
+               }, 1000);
+           }).then(data => ({data}), (err) =>( {error:true,data:err}))
+       }
+   },
    minus() {
        return { type: types.MINUS };
    }
};
export default actions;

5.3 counter.js #

src\store\reducers\counter.js

import * as actionTypes from '../action-types';
let initialState = { number: 0 }
function counter(state = initialState, action) {
    switch (action.type) {
        case actionTypes.ADD:
+           if(action.payload?.error){
+               return { number: state.number - action.payload.data };
+           }
+           return { number: state.number + action.payload.data };
        case actionTypes.MINUS:
            return { number: state.number - 1 };
        default:
            return state;
    }
}
export default counter;

5.4 Counter.js #

src\components\Counter.js

import React from 'react';
import {connect} from 'react-redux';
import actions from '../store/actions/counter';
function Counter(props) {
+    const {number,add,minus,thunkAdd,promiseAdd,payloadPromiseAdd} = props;
    return (
        <div>
            <p>{number}</p>
            <button onClick={add}>+</button>
            <button onClick={thunkAdd}>thunkAdd</button>
+            <button onClick={promiseAdd}>promiseAdd</button>
+            <button onClick={payloadPromiseAdd}>payloadPromiseAdd</button>
            <button onClick={minus}>-</button>
        </div>
    );
}
export default connect(
    state => state.counter,
    actions
)(Counter);

6. redux-first-history #

redux-first-history 是一个用于整合 React Router v4/v5 和 Redux 的库。它以 Redux-first 的方式将你的路由状态同步到你的 Redux store 中,这意味着你可以直接通过 dispatch actions 来改变你的 URL,同时 URL 的变化也会被反映到 Redux 的 state 中。这使得你可以像处理普通的 Redux state 一样处理你的路由状态。

redux-first-history 提供了一些 action creators,如 pushreplacegogoBackgoForward,你可以通过 dispatch 这些 actions 来改变 URL。同时,当 URL 改变时,LOCATION_CHANGE action 会被自动 dispatch,并将新的 location 对象作为 payload。

在 reducer 中,你可以通过 state.router.location 获取当前的 location 对象,这个对象包括了当前的 pathname、search、hash 和 state。

总的来说,redux-first-history 提供了一种整合 React Router 和 Redux 的方法,使得你可以像处理普通的 Redux state 一样处理路由状态。同时,它还提供了一些有用的功能,如历史记录管理、延迟路由跳转等。

6.1 src\index.js #

import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import Counter from './components/Counter';
+import {store,history} from './store';
+import { HistoryRouter } from "redux-first-history/rr6";
+import { Routes, Route, Link } from "react-router-dom";
import Home from './components/Home';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
+    <Provider store={store}>
+        <HistoryRouter history={history}>
+            <ul>
+                <li><Link to="/">Home</Link></li>
+                <li><Link to="/counter">Counter</Link></li>
+            </ul>
+            <Routes>
+                <Route path="/" element={<Home />} />
+                <Route path="/counter" element={<Counter />} />
+            </Routes>
+        </HistoryRouter>
+    </Provider>
);

6.2 src\history.js #

src\history.js

import { createBrowserHistory } from 'history';
import { createReduxHistoryContext } from "redux-first-history";
const history = createBrowserHistory();
const { routerReducer, routerMiddleware, createReduxHistory } = createReduxHistoryContext({ history });
export {
    routerReducer,
    routerMiddleware,
    createReduxHistory
}

6.3 store\index.js #

src\store\index.js

import { legacy_createStore as createStore, applyMiddleware } from 'redux';
import logger from 'redux-logger';
import thunk from 'redux-thunk';
import promise from 'redux-promise';
+import { routerMiddleware, createReduxHistory } from '../history';
import rootReducer from './reducers';
+const store = applyMiddleware(routerMiddleware,thunk,promise,logger)(createStore)(rootReducer);
+const history = createReduxHistory(store);
+export {
+    store,
+    history
+}

6.4 reducers\index.js #

src\store\reducers\index.js

import { combineReducers } from "redux";
import counter from "./counter";
+import { routerReducer } from '../../history';
let rootReducer = combineReducers({
    counter,
+   router: routerReducer
});
export default rootReducer;

6.5 Home.js #

src\components\Home.js

import React from 'react';
import { useDispatch } from 'react-redux';
import { push } from "redux-first-history";
function Home() {
    const dispatch = useDispatch();
    const gotoCounter = () => {
        dispatch(push('/counter'));
    }
    return (
        <div>
            <p>Home</p>
            <button onClick={gotoCounter}>跳转到/counter</button>
        </div>
    )
}
export default Home;