简单用 React+Redux+TypeScript 实现一个 TodoApp (二)
前言
上一篇文章
讲了讲如何用 TypeScript + Redux 实现
Loading
切片部分的状态, 这篇文章主要想聊一聊关于
Todo
和
Filter
这两个切片状态的具体实现, 以及关于
Redux Thunk
与 TypeScript 的结合使用.
想跳过文章直接看代码的: 完整代码
最后的效果:
Todo
首先思考一下
Todo
应该是怎样的状态, 以及可能需要涉及到的
action
.
页面上的每一个
todo
实例都对应一个状态, 合起来总的状态就应该是一个数组, 这也应该是
reducer
最后返回的状态形式. 同时, 考虑
action
, 应该有以下几种操作:
-
初始化页面的时候从服务端拿数据设置所有的
todos -
增加一个
todo -
删除一个
todo -
更新一个
todo -
完成 / 未完成一个
todo
这里需要注意的是, 所有的操作都需要和服务端交互, 因此我们的
action
是
"不纯的"
, 涉及到异步操作. 这里会使用
Redux Thunk
这个库来加持一下.
Action Creator
写法也会变成对应的
Thunk
形式的
Action Creator
types
每一个
todo
的状态类型应该如下:
// store/todo/types.ts
export type TodoState = {
id: string;
text: string;
done: boolean;
id
一般是服务端返回的, 不做过多解释.
text
是
todo
的具体内容,
done
属性描述这个
todo
是否被完成
actions
actionTypes
还是和之前一样, 在写
action
之前先写好对应的类型, 包括每一个
action
的
type
属性
根据上面的描述,
type
有如下几种:
// store/todo/constants.ts
export const SET_TODOS = "SET_TODOS";
export type SET_TODOS = typeof SET_TODOS;
export const ADD_TODO = "ADD_TODO";
export type ADD_TODO = typeof ADD_TODO;
export const REMOVE_TODO = "REMOVE_TODO";
export type REMOVE_TODO = typeof REMOVE_TODO;
export const UPDATE_TODO = "UPDATE_TODO";
export type UPDATE_TODO = typeof UPDATE_TODO;
export const TOGGLE_TODO = "TOGGLE_TODO";
export type TOGGLE_TODO = typeof TOGGLE_TODO;
对应的
actionTypes
, 就可以引用写好的常量类型了:
// store/todo/actionTypes.ts
import { TodoState } from "./types";
import {
SET_TODOS,
ADD_TODO,
REMOVE_TODO,
UPDATE_TODO,
TOGGLE_TODO
} from "./constants";
export type SetTodosAction = {
type: SET_TODOS;
payload: TodoState[];
export type AddTodoAction = {
type: ADD_TODO;
payload: TodoState;
export type RemoveTodoAction = {
type: REMOVE_TODO;
payload: {
id: string;
export type UpdateTodoAction = {
type: UPDATE_TODO;
payload: {
id: string;
text: string;
export type ToggleTodoAction = {
type: TOGGLE_TODO;
payload: {
id: string;
export type TodoAction =
| SetTodosAction
| AddTodoAction
| RemoveTodoAction
| UpdateTodoAction
| ToggleTodoAction;
actionCreators
这里需要注意,
todo
部分的
actions
分为同步和异步, 先来看同步的:
// store/todo/actions.ts
import {
AddTodoAction,
RemoveTodoAction,
SetTodosAction,
ToggleTodoAction,
UpdateTodoAction
} from "./actionTypes";
import {
ADD_TODO,
REMOVE_TODO,
SET_TODOS,
TOGGLE_TODO,
UPDATE_TODO
} from "./constants";
import { TodoState } from "./types";
export const addTodo = (newTodo: TodoState): AddTodoAction => {
return {
type: ADD_TODO,
payload: newTodo
export const removeTodo = (id: string): RemoveTodoAction => {
return {
type: REMOVE_TODO,
payload: {
export const setTodos = (todos: TodoState[]): SetTodosAction => {
return {
type: SET_TODOS,
payload: todos
export const toggleTodo = (id: string): ToggleTodoAction => {
return {
type: TOGGLE_TODO,
payload: {
export const updateTodo = (id: string, text: string): UpdateTodoAction => {
return {
type: UPDATE_TODO,
payload: {
同步部分没什么好说的, 核心是异步部分, 我们用 Redux Thunk 这个中间件帮助我们编写 Thunk 类型的
Action
. 这种
Action
不再是纯的,
同时这个
Action
是一个函数而不再是一个对象
, 因为存在往服务端请求数据的副作用逻辑. 这也是 Redux 和 Flow 的一个小区别(Flow 规定
Action
必须是纯的)
首先我们需要配置一下
thunk
, 以及初始化一下
store
// store/index.ts
import { combineReducers, createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import { loadingReducer } from "./loading/reducer";
import { todoReducer } from "./todo/reducer";
const rootReducer = combineReducers({
todos: todoReducer,
loading: loadingReducer,
// filter: filterReducer,
export type RootState = ReturnType<typeof rootReducer>;
export const store = createStore(rootReducer, applyMiddleware(thunk));
Thunk Action Creator
不考虑类型, 如果纯用 JavaScript 写一个
Thunk ActionCreator
, 如下:
export const setTodosRequest = () => {
return dispatch => {
dispatch(setLoading("加载中..."));
return fetch(baseURL)
.then(res => res.json())
.then(data => {
dispatch(setTodos(data));
dispatch(unsetLoading());
这里的
baseURL
在我第一章有说, 用了 mock api 模拟后端的数据, 具体地址可以看文章或者看源码, 同时为了方便, 我直接用浏览器原生的
fetch
做 http 请求了, 当然用
axios
等别的库也是可以的
关于这个函数简单说明一下, 这里的
setTodosRequest
就是一个
Thunk ActionCreator
, 返回的
(dispatch) => {}
就是我们需要的
Thunk Action
, 可以看到这个
Thunk Action
是一个函数, Redux Thunk 允许我们将 Action 写成这种模式
下面为这个
Thunk ActionCreator
添加类型, Redux Thunk 导出的包里有提供两个很重要的泛型类型:
首先是
ThunkDispatch
, 具体定义如下
/**
* The dispatch method as modified by React-Thunk; overloaded so that you can
* dispatch:
* - standard (object) actions: `dispatch()` returns the action itself
* - thunk actions: `dispatch()` returns the thunk's return value
* @template TState The redux state
* @template TExtraThunkArg The extra argument passed to the inner function of
* thunks (if specified when setting up the Thunk middleware)
* @template TBasicAction The (non-thunk) actions that can be dispatched.
export interface ThunkDispatch<
TState,
TExtraThunkArg,
TBasicAction extends Action
<TReturnType>(
thunkAction: ThunkAction<TReturnType, TState, TExtraThunkArg, TBasicAction>,
): TReturnType;
<A extends TBasicAction>(action: A): A;
// This overload is the union of the two above (see TS issue #14107).
<TReturnType, TAction extends TBasicAction>(
action:
| TAction
| ThunkAction<TReturnType, TState, TExtraThunkArg, TBasicAction>,
): TAction | TReturnType;
至于具体怎么实现我不关心, 我关心的是这个东西是啥以及这个泛型接受哪些类型参数, 整理一下如下:
-
这个
dispatch类型是由 Redux Thunk 修改过的类型, 你可以用它dispatch: -
标准的
action(一个对象),dispatch()函数返回这个对象action本身 -
thunk action(一个函数),dispatch()函数返回这个thunk action函数的返回值 -
接受三个参数:
TState,TExtraThunkArg,TBasicAction -
TState: Redux store 的状态(RootState) -
TExtraThunkArg: 初始化 thunk 中间件时, 传个 thunk 的额外参数(这个项目我们没用到) -
TBasicAction: 非 Thunk 类型的 action, 即标准的对象 action 类型
再看一下
ThunkAction
:
/**
* A "thunk" action (a callback function that can be dispatched to the Redux
* store.)
* Also known as the "thunk inner function", when used with the typical pattern
* of an action creator function that returns a thunk action.
* @template TReturnType The return type of the thunk's inner function
* @template TState The redux state
* @template TExtraThunkARg Optional extra argument passed to the inner function
* (if specified when setting up the Thunk middleware)
* @template TBasicAction The (non-thunk) actions that can be dispatched.
export type ThunkAction<
TReturnType,
TState,
TExtraThunkArg,
TBasicAction extends Action
dispatch: ThunkDispatch<TState, TExtraThunkArg, TBasicAction>,
getState: () => TState,
extraArgument: TExtraThunkArg
,
) => TReturnType;
整理一下参数类型和代表的意思:
-
ThunkAction指代的是一个thunk action, 或者也叫做thunk inner function -
四个类型参数:
TReturnType,TState,TExtraThunkArg,TBasicAction -
TReturnType: 这个thunk action函数最后的返回值 -
TState: Redux store 的状态(RootState) -
TExtraThunkArg: 初始化 thunk 中间件时, 传个 thunk 的额外参数(这个项目我们没用到) -
TBasicAction: 非 Thunk 类型的 action, 即标准的对象 action 类型
看完发现, 其实
ThunkAction
和
ThunkDispatch
真的很像, 对应到具体的参数类型:
-
TState我们是有的, 即之前写过的RootState -
TExtraThunkArg我们没有用到, 可以直接给void或者unknown -
TBasicAction我们还没定义, 我见过有用Redux的AnyAction来替代, 但是AnyAction这个 any 有点过分...我搜索了一下没找到官方的最佳实践, 就打算用所有的 Redux 的 Action 类型集合
以及, Redux 官网的 Usage with Redux Thunk 其实已经有写怎么配置类型了. 现在需要做的事情其实就很简单:
-
增加一个
RootAction类型, 为所有的非 Thunk 类型的Action的类型的集合 -
给
ThunkDispatch这个泛型传入正确类型 -
给
ThunkAction这个泛型传入正确类型
store
部分的代码如下:
// store/index.ts
import { combineReducers, createStore, applyMiddleware } from "redux";
import { todoReducer } from "./todo/reducer";
import { loadingReducer } from "./loading/reducer";
import thunk, { ThunkDispatch, ThunkAction } from "redux-thunk";
import { LoadingAction } from "./loading/actionTypes";
import { TodoAction } from "./todo/actionTypes";
const rootReducer = combineReducers({
todos: todoReducer,
loading: loadingReducer,
// filter: filterReducer,
export type RootState = ReturnType<typeof rootReducer>;
export type RootAction = LoadingAction | TodoAction;
export const store = createStore(rootReducer, applyMiddleware(thunk));
export type AppDispatch = ThunkDispatch<RootState, void, RootAction>;
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
void,
RootAction
为了方便, 这里给了两个 alias, 也是根据官网来的, 分别为
AppDispatch
和
AppThunk
现在可以完善之前的
Thunk ActionCreator
的类型了:
export const setTodosRequest = (): AppThunk<Promise<void>> => {
return dispatch => {
dispatch(setLoading("加载中..."));
return fetch(baseURL)
.then(res => res.json())
.then(data => {
dispatch(setTodos(data));
dispatch(unsetLoading());
这里注意一下, 由于我们的
thunk action
, 是有返回值的, 这里是
return fetch()
返回的是一个
promise
, 不过这个
promise
并没有
resolve
任何值, 所以即为
Promise<void>
最后完善一下所有的
actionCreator
:
// store/todo/actions.ts
import {
AddTodoAction,
RemoveTodoAction,
SetTodosAction,
ToggleTodoAction,
UpdateTodoAction
} from "./actionTypes";
import { setLoading, unsetLoading } from "../loading/actions";
import {
ADD_TODO,
REMOVE_TODO,
SET_TODOS,
TOGGLE_TODO,
UPDATE_TODO
} from "./constants";
import { TodoState } from "./types";
import { AppThunk } from "../index";
import { baseURL } from "../../api";
// https://github.com/reduxjs/redux/issues/3455
export const addTodo = (newTodo: TodoState): AddTodoAction => {
return {
type: ADD_TODO,
payload: newTodo
export const removeTodo = (id: string): RemoveTodoAction => {
return {
type: REMOVE_TODO,
payload: {
export const setTodos = (todos: TodoState[]): SetTodosAction => {
return {
type: SET_TODOS,
payload: todos
export const toggleTodo = (id: string): ToggleTodoAction => {
return {
type: TOGGLE_TODO,
payload: {
export const updateTodo = (id: string, text: string): UpdateTodoAction => {
return {
type: UPDATE_TODO,
payload: {
export const setTodosRequest = (): AppThunk<Promise<void>> => {
return dispatch => {
dispatch(setLoading("加载中..."));
return fetch(baseURL)
.then(res => res.json())
.then(data => {
dispatch(setTodos(data));
dispatch(unsetLoading());
export const addTodoRequest = (text: string): AppThunk<Promise<void>> => {
return dispatch => {
return fetch(baseURL, {
method: "POST",
headers: {
"Content-Type"
: "application/json"
body: JSON.stringify({ text, done: false })
.then(res => res.json())
.then((data: TodoState) => {
dispatch(addTodo(data));
export const removeTodoRequest = (todoId: string): AppThunk<Promise<void>> => {
return dispatch => {
return fetch(`${baseURL}/${todoId}`, {
method: "DELETE"
.then(res => res.json())
.then(({ id }: TodoState) => {
dispatch(removeTodo(id));
export const updateTodoRequest = (
todoId: string,
text: string
): AppThunk<Promise<void>> => {
return dispatch => {
return fetch(`${baseURL}/${todoId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json"
body: JSON.stringify({ text })
.then(res => res.json())
.then(({ id, text }: TodoState) => {
dispatch(updateTodo(id, text));
export const toogleTodoRequest = (
todoId: string,
done: boolean
): AppThunk<Promise<void>> => {
return dispatch => {
return fetch(`${baseURL}/${todoId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json"
body: JSON.stringify({ done })
.then(res => res.json())
.then(({ id }: TodoState) => {
dispatch(toggleTodo(id));
这里说一点题外话, 其实 Redux 不用 Thunk 这种 middleware 来做异步请求也是可以的, 但是为啥还会有
Redux Thunk
这些库存在呢. 具体细节我之前写过一个回答, 有兴趣可以看一看:
redux中间件对于异步action的意义是什么?
reducer
编写完复杂的
ActionCreator
,
reducer
相比就简单很多了, 这里直接贴代码了:
// store/todo/reducer.ts
import { Reducer } from "redux";
import { TodoAction } from "./actionTypes";
import {
ADD_TODO,
REMOVE_TODO,
SET_TODOS,
TOGGLE_TODO,
UPDATE_TODO
} from "./constants";
import { TodoState } from "./types";
const initialState = [];
export const todoReducer: Reducer<Readonly<TodoState>[], TodoAction> = (
state = initialState,
action
) => {
switch (action.type) {
case SET_TODOS:
return action.payload;
case ADD_TODO:
return [...state, action.payload];
case REMOVE_TODO:
return state.filter(todo => todo.id !== action.payload.id);
case UPDATE_TODO:
return state.map(todo => {
if (todo.id === action.payload.id) {
return { ...todo, text: action.payload.text };
return todo;
case TOGGLE_TODO:
return state.map(todo => {
if (todo.id === action.payload.id) {
return { ...todo, done: !todo.done };
return todo;
default:
return state;
写完
reducer
记得在
store
中写入
combineReducer()
selectors
最后是
selectors
, 由于这部分是需要和
filter
切片进行协作,
filter
部分下面会讲, 这里先贴代码, 最后可以再回顾
// store/todo/selectors.ts
import { RootState } from "../index";
export const selectFilteredTodos = (state: RootState) => {
switch (state.filter.status) {
case "all":
return state.todos;
case "active":
return state.todos.filter(todo => todo.done === false);
case "done":
return state.todos.filter(todo => todo.done === true);
default:
return state.todos;
export const selectUncompletedTodos = (state: RootState) => {
return state.todos.filter(todo => todo.done === false);
todo 部分基本完成了, 最后有一个点, Redux 文档中其实一直有提到, 不过之前我一直忽略, 这次看了 redux 文档到底说了什么(上) 文章才有注意到, 就是 Normalizing State Shape . 这部分是关于性能优化的, 我自己的项目包括实习的公司项目其实从来都没有做过这一部分, 因此实战经验为 0. 有兴趣的可以去看看
Filter
最后一个状态切片
filter
, 这部分主要是为了帮助选择展示的 todo 部分. 由于这部分较为简单, 和
loading
部分类似, 居多为代码的罗列
types
回顾之前想要实现的效果, TodoApp 底部是一个类似 tab 的组件, 点击展示不同状态的 todos. 总共是三部分:
- 全部(默认)
- 未完成
- 已完成
编写一下具体的类型:
// store/filter/types.ts
export type FilterStatus = "all" | "active" | "done";
export type FilterState = {
status: FilterStatus;
actions
actionTypes
// store/filter/constants.ts
export const SET_FILTER = "SET_FILTER";
export type SET_FILTER = typeof SET_FILTER;
export const RESET_FILTER = "RESET_FILTER";
export type RESET_FILTER = typeof RESET_FILTER;
// store/filter/actionTypes.ts
import { SET_FILTER, RESET_FILTER } from "./constants";
import { FilterStatus } from "./types";
export type SetFilterAction = {
type: SET_FILTER;
payload: FilterStatus;
export type ResetFilterAction = {
type: RESET_FILTER;
export type FilterAction = SetFilterAction | ResetFilterAction;
actions
// store/filter/actions.ts
import { SetFilterAction, ResetFilterAction } from "./actionTypes";
import { SET_FILTER, RESET_FILTER } from "./constants";
import { FilterStatus } from "./types";
export const setFilter = (filterStatus: FilterStatus): SetFilterAction => {
return {
type: SET_FILTER,
payload: filterStatus
export const resetFilter = (): ResetFilterAction => {
return {
type: RESET_FILTER
reducer
import { Reducer } from "redux";
import { FilterAction } from "./actionTypes";
import { SET_FILTER, RESET_FILTER } from "./constants";
import { FilterState } from "./types";
const initialState: FilterState = {
status: "all"
export const filterReducer: Reducer<Readonly<FilterState>, FilterAction> = (
state = initialState,
action
) => {
switch (action.type) {
case SET_FILTER:
return {
status: action.payload
case RESET_FILTER:
return {
status: "done"
default:
return state;
Store
最后将所有
store
底下的
actions
,
reducers
集成一下,
store
文件如下:
import { combineReducers, createStore, applyMiddleware } from "redux";
import { todoReducer } from "./todo/reducer";
import { filterReducer } from "./filter/reducer";
import { loadingReducer } from "./loading/reducer";
import thunk, { ThunkDispatch, ThunkAction } from "redux-thunk";
import { FilterAction } from "./filter/actionTypes";
import { LoadingAction } from "./loading/actionTypes";
import { TodoAction } from "./todo/actionTypes";
const rootReducer = combineReducers({
todos: todoReducer,
filter: filterReducer,
loading: loadingReducer
export type RootState = ReturnType<typeof rootReducer>;
export type RootAction = FilterAction | LoadingAction | TodoAction;
export const store = createStore(rootReducer, applyMiddleware(thunk));
export type AppDispatch = ThunkDispatch<RootState, void, RootAction>;
export type AppThunk<ReturnType = void> = ThunkAction<