- To-do
- 阅读 Redux 的中间件 文档。
这个案例来自 Mosh 的 Redux 课程。源码存放在 这里。有几点值得一学:
- 如何组织 action、action creator、reducer 及 selector
- 如何编写 middleware 实现异步请求
如何组织各元素
store/bugs.js
:
import { createSlice } from "@reduxjs/toolkit";
import { createSelector } from "reselect";
import axios from "axios";
import { apiCallBegan } from "./api";
import moment from "moment";
const slice = createSlice({
name: "bugs",
initialState: {
list: [],
loading: false,
lastFetch: null
},
reducers: {
bugsRequested: (bugs, action) => {
bugs.loading = true;
},
bugsReceived: (bugs, action) => {
bugs.list = action.payload;
bugs.loading = false;
bugs.lastFetch = Date.now();
},
bugsRequestFailed: (bugs, action) => {
bugs.loading = false;
},
bugAssignedToUser: (bugs, action) => {
const { id: bugId, userId } = action.payload;
const index = bugs.list.findIndex(bug => bug.id === bugId);
bugs.list[index].userId = userId;
},
// command - event
// addBug - bugAdded
bugAdded: (bugs, action) => {
bugs.list.push(action.payload);
},
// resolveBug (command) - bugResolved (event)
bugResolved: (bugs, action) => {
const index = bugs.list.findIndex(bug => bug.id === action.payload.id);
bugs.list[index].resolved = true;
}
}
});
export const {
bugAdded,
bugResolved,
bugAssignedToUser,
bugsReceived,
bugsRequested,
bugsRequestFailed
} = slice.actions;
export default slice.reducer;
// Action Creators
const url = "/bugs";
export const loadBugs = () => (dispatch, getState) => {
const { lastFetch } = getState().entities.bugs;
const diffInMinutes = moment().diff(moment(lastFetch), "minutes");
if (diffInMinutes < 10) return;
return dispatch(
apiCallBegan({
url,
onStart: bugsRequested.type,
onSuccess: bugsReceived.type,
onError: bugsRequestFailed.type
})
);
};
export const addBug = bug =>
apiCallBegan({
url,
method: "post",
data: bug,
onSuccess: bugAdded.type
});
export const resolveBug = id =>
apiCallBegan({
// /bugs
// PATCH /bugs/1
url: url + "/" + id,
method: "patch",
data: { resolved: true },
onSuccess: bugResolved.type
});
export const assignBugToUser = (bugId, userId) =>
apiCallBegan({
url: url + "/" + bugId,
method: "patch",
data: { userId },
onSuccess: bugAssignedToUser.type
});
// Selector
// Memoization
// bugs => get unresolved bugs from the cache
export const getBugsByUser = userId =>
createSelector(
state => state.entities.bugs,
bugs => bugs.filter(bug => bug.userId === userId)
);
export const getUnresolvedBugs = createSelector(
state => state.entities.bugs,
state => state.entities.projects,
(bugs, projects) => bugs.list.filter(bug => !bug.resolved)
);
这串代码非常长。作者使用 Redux Toolkit 来简化代码。
在命名规范上:
- Action:使用被动语态的单词,如
bugAdded
、bugResolved
- Action creators:使用动词短语,如
addBug
、assignBugToUser
- Selector:使用
getXXX
这种查询句式,如getUnresolvedBugs
在 export
的内容上:
- Reducer 是被
export default
的 - 具体的各 action 是被
export
单个符号的- 但是在这个场景中,各 action 实际上不被外部代码直接用到,而是通过 action creator 被使用,我认为也可以不 export 出去
如何编写 middleware 实现异步请求
由于 reducer 中的函数实现是 不能有副作用 的,像发起异步请求这种操作,需要放在 middleware 中做。
首先在 store/api.js
中定义一套 action:
import { createAction } from "@reduxjs/toolkit";
export const apiCallBegan = createAction("api/callBegan");
export const apiCallSuccess = createAction("api/callSuccess");
export const apiCallFailed = createAction("api/callFailed");
在 store/middleware/api.js
中定义中间件:
import axios from "axios";
import * as actions from "../api";
const api = ({ dispatch }) => next => async action => {
if (action.type !== actions.apiCallBegan.type) return next(action);
const { url, method, data, onStart, onSuccess, onError } = action.payload;
if (onStart) dispatch({ type: onStart });
next(action);
try {
const response = await axios.request({
baseURL: "http://localhost:9001/api",
url,
method,
data
});
// General
dispatch(actions.apiCallSuccess(response.data));
// Specific
if (onSuccess) dispatch({ type: onSuccess, payload: response.data });
} catch (error) {
// General
dispatch(actions.apiCallFailed(error.message));
// Specific
if (onError) dispatch({ type: onError, payload: error.message });
}
};
export default api;