1.Redux Toolkit #

2. 安装Redux Toolkit #

npm install redux react-redux redux-logger redux-thunk @reduxjs/toolkit 
npm install express cors  axios 

3.普通用法 #

3.1 public\index.html #

public\index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta name="description" content="Web site created using create-react-app" />
    <title>React App</title>
</head>

<body>
    <div>
        <p id="value">0</p>
        <button id="add">+</button>
        <button id="minus">-</button>
      </div>
</body>
</html>

3.2 src\index.js #

src\index.js

import { legacy_createStore as createStore } from 'redux'
const ADD = 'ADD'
const MINUS = 'MINUS'

function add() {
    return { type: ADD }
}

function minus() {
    return { type: MINUS }
}
function counter(state = { number: 0 }, action) {
    switch (action.type) {
        case ADD:
            return { number: state.number + 1 }
        case MINUS:
            return { number: state.number - 1 }
        default:
            return state
    }
}

var store = createStore(counter)
var valueEl = document.getElementById('value')

function render() {
    valueEl.innerHTML = store.getState().number;
}

render()
store.subscribe(render)

document.getElementById('add').addEventListener('click', function () {
    store.dispatch(add())
})

document.getElementById('minus').addEventListener('click', function () {
    store.dispatch(minus())
})

3.3 痛点 #

4.configureStore #

4.1 public\index.html #

public\index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta name="description" content="Web site created using create-react-app" />
    <title>React App</title>
</head>

<body>
    <div>
        <p id="value">0</p>
        <button id="add">+</button>
        <button id="minus">-</button>
+       <button id="async-add">async-add</button>
      </div>
</body>
</html>

4.2 src\index.js #

src\index.js

+import { configureStore } from './@reduxjs/toolkit';
+import {thunk} from 'redux-thunk';
+import logger from 'redux-logger';
const ADD = 'ADD'
const MINUS = 'MINUS'
function add() {
    return { type: ADD }
}
function minus() {
    return { type: MINUS }
}
function counter(state = { number: 0 }, action) {
    switch (action.type) {
        case ADD:
            return { number: state.number + 1 }
        case MINUS:
            return { number: state.number - 1 }
        default:
            return state
    }
}
+const store = configureStore({
+    reducer: counter,
+    middleware:()=> [thunk, logger]
+})
var valueEl = document.getElementById('value')
function render() {
    valueEl.innerHTML = store.getState().number;
}
render()
store.subscribe(render)
document.getElementById('add').addEventListener('click', function () {
    store.dispatch(add())
})
document.getElementById('minus').addEventListener('click', function () {
    store.dispatch(minus())
})
+document.getElementById('async-add').addEventListener('click', function () {
+  store.dispatch((dispatch)=>{
+    setTimeout(()=>{
+      dispatch(add())
+    },1000)
+  })
+})

4.3 toolkit\index.js #

src\@reduxjs\toolkit\index.js

export { default as configureStore } from './configureStore';

4.4 configureStore.js #

src\@reduxjs\toolkit\configureStore.js

import { combineReducers, applyMiddleware, legacy_createStore } from 'redux';
import {thunk} from 'redux-thunk';
function isPlainObject(value) {
    if (typeof value !== "object" || value === null)
        return false;
    return Object.getPrototypeOf(value) === Object.prototype;
}
function configureStore(options = {}) {
    let { reducer, middleware, preloadedState } = options;
    let rootReducer;
    if (typeof reducer === "function") {
        rootReducer = reducer;
    } else if (isPlainObject(reducer)) {
        rootReducer = combineReducers(reducer);
    }
    const defaultMiddlewares = [thunk];
    return applyMiddleware(...typeof middleware ==='function'?middleware(defaultMiddlewares):defaultMiddlewares)(legacy_createStore)(rootReducer, preloadedState);
}
export default configureStore;

5.createAction #

5.1 src\index.js #

src\index.js

+import { configureStore,createAction } from './@reduxjs/toolkit';
import {thunk} from 'redux-thunk';
import logger from 'redux-logger';
+const add = createAction('ADD')
+const minus = createAction('MINUS', (amount) => ({ payload: amount }))
+console.log(minus.toString());
+console.log(minus.type);
function counter(state = { number: 0 }, action) {
    switch (action.type) {
+        case add.type:
            return { number: state.number + 1 }
+        case minus.type:
+            return {number:state.number-action.payload}
        default:
            return state
    }
}
const store = configureStore({
    reducer: counter,
    middleware:()=> [thunk, logger]
})
var valueEl = document.getElementById('value')
function render() {
    valueEl.innerHTML = store.getState().number;
}
render()
store.subscribe(render)
document.getElementById('add').addEventListener('click', function () {
    store.dispatch(add())
})
document.getElementById('minus').addEventListener('click', function () {
+    store.dispatch(minus(2))
})
document.getElementById('async-add').addEventListener('click', function () {
  store.dispatch((dispatch)=>{
    setTimeout(()=>{
      dispatch(add())
    },1000)
  })
})

5.2 toolkit\index.js #

src\@reduxjs\toolkit\index.js

export { default as configureStore } from './configureStore';
+export { default as createAction } from './createAction';

5.3 createAction.js #

src\@reduxjs\toolkit\createAction.js

function createAction(type, prepareAction) {
    function actionCreator(...args) {
        if (prepareAction) {
            var prepared = prepareAction.apply(null, args);
            return {
                type,
                ...prepared
            };
        }
        return {
            type,
            payload: args[0]
        };
    }
    actionCreator.toString = function () {
        return "" + type;
    }
    actionCreator.type = type;
    return actionCreator;
}
export default createAction;

6.createReducer #

6.1 src\index.js #

src\index.js

+import { configureStore,createAction,createReducer } from './@reduxjs/toolkit';
import {thunk} from 'redux-thunk';
import logger from 'redux-logger';
const add = createAction('ADD')
const minus = createAction('MINUS', (amount) => ({ payload: amount }))
console.log(minus.toString());
console.log(minus.type);
+const counter = createReducer({number:0}, {
+  [add]: state => ({number:state.number+1}),
+  [minus]: state => ({number:state.number-1})
+})
const store = configureStore({
    reducer: counter,
    middleware:()=> [thunk, logger]
})
var valueEl = document.getElementById('value')
function render() {
    valueEl.innerHTML = store.getState().number;
}
render()
store.subscribe(render)
document.getElementById('add').addEventListener('click', function () {
    store.dispatch(add())
})
document.getElementById('minus').addEventListener('click', function () {
    store.dispatch(minus(2))
})
document.getElementById('async-add').addEventListener('click', function () {
  store.dispatch((dispatch)=>{
    setTimeout(()=>{
      dispatch(add())
    },1000)
  })
})

6.2 toolkit\index.js #

src\@reduxjs\toolkit\index.js

export { default as configureStore } from './configureStore';
export { default as createAction } from './createAction';
+export { default as createReducer } from './createReducer';

6.3 createReducer.js #

src\@reduxjs\toolkit\createReducer.js

function createReducer(initialState, reducers={}) {
    return function (state = initialState, action) {
        let reducer = reducers[action.type];
        if (reducer) return reducer(state, action);
        return state;
    }
}
export default createReducer;
import produce from 'immer';
function createReducer(initialState, builderCallback) {
  // 用于存储每种 action 类型对应的处理函数
  const actionsMap = new Map();
  // 执行 builderCallback 来填充 actionsMap
  const builder = {
    addCase(action, reducer) {
      actionsMap.set(action.type, reducer);
      return this; // 允许链式调用
    }
  };
  builderCallback(builder);
  // 返回实际的 reducer 函数
  return function reducer(state = initialState, action) {
    // 检查这个 action 类型是否有对应的处理函数
    const caseReducer = actionsMap.get(action.type);
    if (caseReducer) {
      // 使用 Immer 的 produce 来保证状态的不可变性
      return produce(state, draftState => {
        caseReducer(draftState, action);
      });
    }
    return state;
  };
}
export default createReducer;

7.createSlice #

7.1 src\index.js #

src\index.js

+import { configureStore, createSlice} from './@reduxjs/toolkit';
import { thunk } from 'redux-thunk';
import logger from 'redux-logger';
+const counterSlice = createSlice({
+  name: 'counter',
+  initialState: { number: 0 },
+  reducers: {
+    add: (state) => ({ number: state.number + 1 }),
+    minus: (state, action) => ({ number: state.number - action.payload })
+  }
+})
+const { actions, reducer } = counterSlice
+const { add, minus } = actions
const store = configureStore({
+ reducer,
  middleware: () => [thunk, logger]
})
var valueEl = document.getElementById('value')
function render() {
  valueEl.innerHTML = store.getState().number;
}
render()
store.subscribe(render)
document.getElementById('add').addEventListener('click', function () {
  store.dispatch(add())
})
document.getElementById('minus').addEventListener('click', function () {
  store.dispatch(minus(2))
})
document.getElementById('async-add').addEventListener('click', function () {
  store.dispatch((dispatch) => {
    setTimeout(() => {
      dispatch(add())
    }, 1000)
  })
})

7.2 toolkit\index.js #

src\@reduxjs\toolkit\index.js

export { default as configureStore } from './configureStore';
export { default as createAction } from './createAction';
export { default as createReducer } from './createReducer';
+export { default as createSlice } from './createSlice';

7.3 createSlice.js #

src\@reduxjs\toolkit\createSlice.js

import { createReducer, createAction } from './'
function createSlice(options) {
    let { name, initialState = {}, reducers = {} } = options;
    let actions = {};
    const prefixReducers = {};
    Object.keys(reducers).forEach(function (key) {
        var type = getType(name, key);
        actions[key] = createAction(type);
        prefixReducers[type] = reducers[key];
    })
    let reducer = createReducer(initialState, prefixReducers);
    return {
        name,
        reducer,
        actions
    };
}
function getType(slice, actionKey) {
    return slice + "/" + actionKey;
}
export default createSlice;

8. immer #

8.1 原理 #

8.1.1 介绍 #

8.1.2 produce #

let {produce} = require('immer');
let baseState = {
    ids: [1],
    pos: {
        x: 1,
        y: 1
    }
}
let nextState = produce(baseState, (draft) => {
    draft.ids.push(2);
})
console.log(baseState.ids === nextState.ids);//false
console.log(baseState.pos === nextState.pos);//true
function produce(baseState, producer) {
    // 创建一个深拷贝的草稿
    let draft = JSON.parse(JSON.stringify(baseState));
    // 应用修改函数
    producer(draft);
    // 遍历对象属性,比较修改前后的差异
    // 并根据需要创建新的不可变状态
    const nextState = {};
    for (let key in baseState) {
        if (JSON.stringify(baseState[key]) !== JSON.stringify(draft[key])) {
            nextState[key] = draft[key];
        } else {
            nextState[key] = baseState[key];
        }
    }

    return nextState;
}

// const { produce } = require('immer');
// 检查一个值是否为对象
const isObject = (val) => Object.prototype.toString.call(val) === '[object Object]';
// 检查一个值是否为数组
const isArray = (val) => Array.isArray(val);
// 检查一个值是否为函数
const isFunction = (val) => typeof val === 'function';
// 创建一个唯一的Symbol,用于内部状态的存储
const INTERNAL = Symbol('INTERNAL');
// 将基础状态转换为代理对象的函数
function toProxy(baseState, valueChange) {
    // 存储每个属性的代理对象
    let keyToProxy = {};
    // 内部状态,包含草稿状态、属性代理和是否发生了变化的标志
    let internal = {
        draftState: createDraftState(baseState), // 创建草稿状态
        keyToProxy, // 属性代理映射
        mutated: false // 是否发生变化的标志
    }
    // 返回baseState的代理对象
    return new Proxy(baseState, {
        // 拦截对象属性的读取
        get(target, key) {
            // 特殊情况:如果读取的是INTERNAL标识符,返回内部状态
            if (key === INTERNAL) {
                return internal;
            }
            // 获取目标属性的值
            const value = target[key];
            // 如果值是对象或数组,需要特殊处理
            if (isObject(value) || isArray(value)) {
                // 如果该属性已经被代理过,直接返回代理对象
                if (key in keyToProxy) {
                    return keyToProxy[key];
                } else {
                    // 否则,创建新的代理对象,并存储起来
                    keyToProxy[key] = toProxy(value, () => {
                        // 在子对象变化时,更新内部状态
                        internal.mutated = true;
                        const proxyOfChild = keyToProxy[key];
                        const { draftState } = proxyOfChild[INTERNAL];
                        internal.draftState[key] = draftState;
                        valueChange && valueChange();
                    })
                    return keyToProxy[key];
                }
            } else if (isFunction(value)) {
                // 如果值是函数,绑定到草稿状态,并标记为已变化
                internal.mutated = true;
                valueChange && valueChange();
                return value.bind(internal.draftState);
            }
            // 返回草稿状态或基础状态的属性值
            return internal.mutated ? internal.draftState[key] : baseState[key];
        },
        // 拦截对象属性的设置
        set(target, key, value) {
            // 标记为已变化
            internal.mutated = true;
            // 获取草稿状态
            let { draftState } = internal;
            // 确保草稿状态包含所有原始对象的属性
            for (const key in target) {
                draftState[key] = key in draftState ? draftState[key] : target[key];
            }
            // 设置新的值
            draftState[key] = value;
            valueChange && valueChange();
            // 返回true表示设置成功
            return true;
        }
    });
    // 创建草稿状态的函数
    function createDraftState(baseState) {
        // 如果是数组,返回其副本
        if (isArray(baseState)) {
            return [...baseState];
        } else if (isObject(baseState)) {
            // 如果是对象,返回其副本
            return Object.assign({}, baseState);
        } else {
            // 否则,直接返回原始值
            return baseState;
        }
    }
}
// 主函数,用于生成新的状态
function produce(baseState, producer) {
    // 创建代理对象
    const proxy = toProxy(baseState);
    // 应用修改函数
    producer(proxy);
    // 获取内部状态
    const internal = proxy[INTERNAL];
    // 根据是否发生变化返回新的状态或原始状态
    return internal.mutated ? internal.draftState : baseState;
}
// 示例基础状态
let baseState = {
    ids: [1],
    pos: {
        x: 1,
        y: 1
    }
};
// 生成新的状态
let nextState = produce(baseState, draft => {
    draft.ids.push(2);
});
// 输出比较结果
console.log(baseState.ids === nextState.ids); // 比较ids数组是否相同
console.log(baseState.pos === nextState.pos); // 比较pos对象是否相同

8.2 使用 #

8.2.1 src\index.js #

src\index.js

import { configureStore, createSlice} from './@reduxjs/toolkit';
import { thunk } from 'redux-thunk';
import logger from 'redux-logger';
const counterSlice = createSlice({
  name: 'counter',
  initialState: { number: 0 },
  reducers: {
+   add: (state) => state.number+=1,
+   minus: (state, action) => state.number-=action.payload
  }
})
const { actions, reducer } = counterSlice
const { add, minus } = actions
const store = configureStore({
  reducer,
  middleware: () => [thunk, logger]
})
var valueEl = document.getElementById('value')
function render() {
  valueEl.innerHTML = store.getState().number;
}
render()
store.subscribe(render)
document.getElementById('add').addEventListener('click', function () {
  store.dispatch(add())
})
document.getElementById('minus').addEventListener('click', function () {
  store.dispatch(minus(2))
})
document.getElementById('async-add').addEventListener('click', function () {
  store.dispatch((dispatch) => {
    setTimeout(() => {
      dispatch(add())
    }, 1000)
  })
})

8.2.2 createReducer.js #

src\@reduxjs\toolkit\createReducer.js

+import produce from 'immer';
function createReducer(initialState, reducers = {}) {
    return function (state = initialState, action) {
        let reducer = reducers[action.type];
        if (reducer) {
+           return produce(state, draft => {
+               reducer(draft, action);
+           });
        }
        return state;
    }
}
export default createReducer;

9. reselect #

9.1 基础 #

///const {createSelector} = require('reselect');
function createSelector(selectors, reducer) {
    let lastState;
    let lastValue;
    return function (state) {
        if (lastState === state) {
            return lastValue;
        }
        let values = selectors.map(selector => selector(state));
        lastValue = reducer(...values);
        lastState = state;
        return lastValue;
    }
}
const selectCounter1 = state => state.counter1
const selectCounter2 = state => state.counter2
const totalSelector = createSelector(
    [selectCounter1, selectCounter2],
    (counter1, counter2) => {
        console.log('计算结果');
        return counter1.number + counter2.number;
    }
)
let state = { counter1: { number: 1 }, counter2: { number: 2 } };
let state1 = totalSelector(state);
console.log(state1);
let state2 = totalSelector(state);
console.log(state2);

9.2 使用 #

9.2.1 public\index.html #

public\index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <title>React App</title>
  </head>
  <body>
    <div>
+     <p id="value1">0</p>
+     <button id="add1">add1</button>
+     <button id="minus1">minus1</button>
+     <hr />
+     <p id="value2">0</p>
+     <button id="add2">add2</button>
+     <button id="minus2">minus2</button>
+     <hr />
+     <p id="sum">0</p>
    </div>
  </body>
</html>

9.2.2 src\index.js #

src\index.js

import { configureStore, createSlice,createSelector} from './@reduxjs/toolkit';
import { thunk } from 'redux-thunk';
import logger from 'redux-logger';
const counter1Slice = createSlice({
  name: 'counter1',
  initialState: { number: 0 },
  reducers: {
    add: state => { state.number += 1 },
    minus: state => { state.number -= 1 }
  }
})
const counter2Slice = createSlice({
  name: 'counter2',
  initialState: { number: 0 },
  reducers: {
    add: state => { state.number += 1 },
    minus: state => { state.number -= 1 }
  }
})
const { actions: { add: add1, minus: minus1 }, reducer: reducer1 } = counter1Slice
const { actions: { add: add2, minus: minus2 }, reducer: reducer2 } = counter2Slice
const store = configureStore({
  reducer: { counter1: reducer1, counter2: reducer2 },
  middleware: ()=>[thunk, logger]
})
var value1El = document.getElementById('value1')
var value2El = document.getElementById('value2')
var sumEl = document.getElementById('sum')
const selectCounter1 = state => state.counter1
const selectCounter2 = state => state.counter2
const totalSelector = createSelector(
  [selectCounter1, selectCounter2],
  (counter1, counter2) => {
    return counter1.number + counter2.number;
  }
)
function render() {
  value1El.innerHTML = store.getState().counter1.number;
  value2El.innerHTML = store.getState().counter2.number;
  sumEl.innerHTML = totalSelector(store.getState());
}
render()
store.subscribe(render)
document.getElementById('add1').addEventListener('click', function () {
  store.dispatch(add1())
})
document.getElementById('minus1').addEventListener('click', function () {
  store.dispatch(minus1())
})
document.getElementById('add2').addEventListener('click', function () {
  store.dispatch(add2())
})
document.getElementById('minus2').addEventListener('click', function () {
  store.dispatch(minus2())
})

9.2.3 toolkit\index.js #

src\@reduxjs\toolkit\index.js

export { default as configureStore } from './configureStore';
export { default as createAction } from './createAction';
export { default as createReducer } from './createReducer';
export { default as createSlice } from './createSlice';
+export {createSelector } from '../../reselect';

9.2.4 reselect\index.js #

src\reselect\index.js

export {default as createSelector} from './createSelector.js';

9.2.5 createSelector.js #

src\reselect\createSelector.js

function createSelector(selectors, reducer) {
    let lastState;
    let lastValue;
    return function (state) {
        if (lastState === state) {
            return lastValue;
        }
        let values = selectors.map(selector => selector(state));
        lastValue = reducer(...values);
        lastState = state;
        return lastValue;git add
    }
}
export default createSelector;

10. createAsyncThunk #

10.1 src\index.js #

src\index.js

import { configureStore, createSlice, createSelector, createAsyncThunk } from './@reduxjs/toolkit';
import axios from 'axios';
const getTodosList = createAsyncThunk(
  "todos/list", async () => await axios.get(`http://localhost:8080/todos/list`)
);
const initialState = {
  todos: [],
  loading: false,
  error: null,
};
const todoSlice = createSlice({
  name: 'todos',
  initialState,
  reducers: {},
  extraReducers: {
    [getTodosList.pending]: (state) => {
      state.loading = true;
    },
    [getTodosList.fulfilled]: (state, action) => {
      state.todos = action.payload.data;
      state.loading = false;
    },
    [getTodosList.rejected]: (state, action) => {
      state.todos = [];
      state.error = action.error.message;
      state.loading = false;
    }
  }
})
const { reducer } = todoSlice;
const store = configureStore({
  reducer
})
let promise = store.dispatch(getTodosList());
console.log('请求开始', store.getState());
//promise.abort();
promise.then((response) => {
  console.log('成功', response);
  setTimeout(() => {
    console.log('请求结束', store.getState());
  },);
}, error => {
  console.log('失败', error);
  setTimeout(() => {
    console.log('请求结束', store.getState());
  },);
});

10.2 toolkit\index.js #

src\@reduxjs\toolkit\index.js

export { default as configureStore } from './configureStore';
export { default as createAction } from './createAction';
export { default as createReducer } from './createReducer';
export { default as createSlice } from './createSlice';
export {createSelector } from '../../reselect';
+export { default as createAsyncThunk } from "./createAsyncThunk";

10.3 createSlice.js #

src\@reduxjs\toolkit\createSlice.js

import { createReducer, createAction } from './'
function createSlice(options) {
+   let { name, initialState = {}, reducers = {},extraReducers={}   } = options;
    let actions = {};
    const prefixReducers = {};
    Object.keys(reducers).forEach(function (key) {
        var type = getType(name, key);
        actions[key] = createAction(type);
        prefixReducers[type] = reducers[key];
    })
+   let reducer = createReducer(initialState, prefixReducers,extraReducers);
    return {
        name,
        reducer,
        actions
    };
}
function getType(slice, actionKey) {
    return slice + "/" + actionKey;
}
export default createSlice;

10.4 createReducer.js #

src\@reduxjs\toolkit\createReducer.js

import produce from 'immer';
+function createReducer(initialState, reducers = {}, extraReducers = {}) {
    return function (state = initialState, action) {
        let reducer = reducers[action.type];
        if (reducer) {
            return produce(state, draft => {
                reducer(draft, action);
            });
        }
+       let extraReducer = extraReducers[action.type];
+       if (extraReducer) {
+           return produce(state, draft => {
+               extraReducer(draft, action);
+           });
+       }
        return state;
    }
}
export default createReducer;

10.5 createAsyncThunk.js #

src\@reduxjs\toolkit\createAsyncThunk.js

import { createAction } from './';
function createAsyncThunk(typePrefix, payloadCreator) {
    let pending = createAction(typePrefix + "/pending", function () {
        return ({ payload: void 0 });
    });
    let fulfilled = createAction(typePrefix + "/fulfilled", function (payload) {
        return ({ payload });
    });
    let rejected = createAction(typePrefix + "/rejected", function (error) {
        return ({ error });
    });
    function actionCreator(arg) {
        return function (dispatch) {
            dispatch(pending());
            const promise = payloadCreator(arg);
            let abort;
            const abortedPromise = new Promise((_, reject) => {
                abort = () => {
                    reject({ name: "AbortError", message: "Aborted" });
                }
            });
            Promise.race([promise, abortedPromise]).then(result => {
                return dispatch(fulfilled(result));
            }, (error) => {
                return dispatch(rejected(error));
            });
            return Object.assign(promise, { abort });
        }
    }
    return Object.assign(actionCreator, { pending, rejected, fulfilled });
}
export default createAsyncThunk;
import createAction from './createAction';
/**
 * createAsyncThunk 接收redux动作类型和一个返回 Promise的异步请求函数
 * typePrefix 动作类型的前缀
 * payloadCreator 异步请求函数
 * 它会基于你传递的动作类型前缀生成promise生命周期的动作类型
 * 并且最终会返回一个thunk动作的创建者,这个thunk动作的创建者会根据promise函烽并且派发生命周期动作
 * 它抽象了处于异步请求生命周期的标准方法
 */
function createAsyncThunk(typePrefix,payloadCreator){
    const pending = createAction(`${typePrefix}/pending`,()=>{
        return {payload:void 0};
    })
    const fulfilled = createAction(`${typePrefix}/fulfilled`,(payload)=>{
        return {payload};
    })
    const rejected = createAction(`${typePrefix}/rejected`,(error)=>{
        return {error};//action={type:'todos/rejected',error}
    })
    function actionCreator(){//getTodos
        //返回一个thunk函数
        return function(dispatch){//asyncThunk
            dispatch(pending());
            const axiosPromise = payloadCreator();
            return axiosPromise.then(
                result=>dispatch(fulfilled(result)),
                error=>dispatch(rejected(error)),
            )
        }
    }
    return Object.assign(actionCreator,{pending,fulfilled,rejected});
}
export default createAsyncThunk;

10.6 server.js #

server.js

let express = require('express');
let cors = require('cors');
let app = express();
app.use(cors());
let success= true;
app.use((req, res, next) => {
    if(success){
        success = false;
        next();
    }else{
        success = true;
        res.status(500).json({message:'服务器出错'});
    }
});
let todos = [{ id: 1, text: "吃饭" }, { id: 2, text: "睡觉" }];
app.get('/todos/list', (_req, res) => {
    res.json(todos);
});
app.get('/todos/detail/:id', (req, res) => {
    let id = req.params.id;
    let todo = todos.find(item => item.id === parseInt(id));
    res.json(todo);
});
app.listen(8080, () => console.log(`服务在端口8080启动`));

11.Redux Toolkit Query #

11.1 public\index.html #

public\index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <title>React App</title>
  </head>
  <body>
+   <div id="root"></div>
  </body>
</html>

11.2 src\index.js #

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

11.3 App.js #

src\App.js

import todosAPI from './todosApi'
function App() {
    const { data, error, isLoading } = todosAPI.endpoints.getTodos.useQuery()
    console.log('isLoading=', isLoading, 'error=', error, 'data=', data);
    if (isLoading) {
        return <div>加载中....</div>;
    } else {
        if (error) {
            return <div>{error.error}</div>;
        } else if (data) {
            return <div>{data.text}</div>;
        } else {
            return null;
        }
    }
}
export default App;

11.4 todosApi.js #

src\todosApi.js

import { createApi, fetchBaseQuery } from './@reduxjs/toolkit/query/react'
const todosApi = createApi({
  reducerPath: 'todosApi',
  baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:8080' }),
  endpoints: (builder) => {
    return {
      getTodos: builder.query({query: () => `/todos/list`}),
      getTodo: builder.query({query: (id) => `/todos/detail/${id}`}),
    }
  }
})
export default todosApi;

11.5 react.js #

src\@reduxjs\toolkit\query\react.js

import { createSlice } from '../'
import { useEffect, useContext, useReducer } from 'react';
import { ReactReduxContext } from 'react-redux';
const FETCH_DATA = 'FETCH_DATA';
function fetchBaseQuery({ baseUrl }) {
    return function (url) {
        return fetch(baseUrl + url);
    }
}
function createApi({ reducerPath, baseQuery, endpoints }) {
    const builder = {
        query(options) {
            function useQuery(...args) {
                const { store } = useContext(ReactReduxContext)
                const [, forceUpdate] = useReducer(x => x + 1, 0);
                useEffect(() => {
                    const url = options.query(...args);
                    store.dispatch({ type: FETCH_DATA, payload: { url } });
                    return store.subscribe(forceUpdate);
                }, [store,...args])
                const state = store.getState();
                return state ? state[reducerPath] : {};
            }
            return { useQuery };
        }
    }
    const slice = createSlice({
        name: reducerPath,
        initialState: { data: undefined, error: undefined, isLoading: false },
        reducers: {
            setValue(state, { payload = {} }) {
                for (let key in payload)
                    state[key] = payload[key];
            }
        }
    });
    const { actions, reducer } = slice
    const api = {
        reducerPath,
        endpoints: endpoints(builder),
        reducer,
        middleware: function ({ dispatch }) {
            return function (next) {
                return function (action) {
                    if (action.type === FETCH_DATA) {
                        let { url } = action.payload;
                        ; (async function () {
                            dispatch(actions.setValue({ isLoading: true }));
                            const response = await baseQuery(url);
                            const { status } = response;
                            const data = await response.json();
                            if (status >= 200 && status < 300) {
                                dispatch(actions.setValue({ data, error: undefined, isLoading: false }));
                            } else {
                                dispatch(actions.setValue({ error: { status, data }, data: undefined, isLoading: false }));
                            }
                        })();
                    } else {
                        next(action);
                    }
                }
            }
        }
    }
    return api;
}
export { fetchBaseQuery, createApi }

11.6 tore.js #

src\store.js

import { configureStore } from './@reduxjs/toolkit'
import todosAPI from './todosApi'
const store = configureStore({
  reducer: {
    [todosAPI.reducerPath]: todosAPI.reducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(todosAPI.middleware)
})
export default store;

12.axios-basequery #

12.1 src\todosApi.js #

src\todosApi.js

+import { createApi } from './@reduxjs/toolkit/query/react'
+import axios from 'axios'
+axios.interceptors.response.use(function (response) {
+  return {data:response.data};
+},function (error){
+  return {error:{error:error.message}};
+});
+const axiosBaseQuery = ({ baseUrl }) => (
+  async (url) => {
+    try {
+      const data = await axios({ url: baseUrl + url })
+      return { data}
+    } catch (error) {
+      return {
+        error: {
+          status: error.response?.status,
+          data: error.response?.data || error.message,
+        },
+      }
+    }
+  }
+)
const todosApi = createApi({
  reducerPath: 'todosApi',
  baseQuery: axiosBaseQuery({ baseUrl: 'http://localhost:8080' }),
  endpoints: (builder) => {
    return {
      getTodos: builder.query({query: () => `/todos/list`}),
      getTodo: builder.query({query: (id) => `/todos/detail/${id}`}),
    }
  }
})
export default todosApi;