跳到主要内容

redux 实践

首先是借助 create-react-app 初始化一个 TS + React 环境的项目

npx create-react-app craapp --template typescript

安装 Redux 相关依赖

yarn add redux react-redux @reduxjs/toolkit
  • redux: 核心状态管理库
  • react-redux: 用于 React 框架的桥接层(负责更新 react)
  • @reduxjs/toolkit: 降低 Redux 使用难度的助手(采用了 slice 思想,和 immer)

全局 Store 的创建

所有的状态都放在了 Store 中,因此需要一个统一的地方来管理,以一个计数器为例,在 ./src/store 下的文件结构如下:

.
├── index.ts // store 实例,导出 state 和 dispatch 类型
└── reducers // 集合所有的 reducer
├── counter.ts // 用于计数器的 reducer、action、selector
└── index.ts // 导出 rootReducers,用于整合所有的 reducer

store 层 store/index.ts

import { configureStore } from "@reduxjs/toolkit";
import rootReducers from "./reducers"; // 引入 reducer 的集合

// 实例化 store,全局唯一
const store = configureStore({
reducer: rootReducers,
});

// 导出 Store 中的状态(state)类型
export type RootState = ReturnType<typeof store.getState>;

// 导出更改状态的 Dispatch 方法类型
export type AppDispatch = typeof store.dispatch;

// 默认导出 store,用于全局的 Provieder 消费
export default store;

root reducer store/reducers/index.ts

import { combineReducers } from "@reduxjs/toolkit";
import counterSlice from "./counter"; // 可以引入各种 reducer

const rootReducers = combineReducers({
counter: counterSlice, // 这里通过 MAP 形式,自定义不同 reducer 的“命名空间”
// ... 可以在这里扩展添加任意的 reducer
});

// 默认导出,给 configureStore 消费
export default rootReducers;

store/reducers/counter.ts slice 层

接下来看看怎么便捷的创建一个 Reducer,以前使用 Redux 总是需要手动创建多个文件,reducer、action、action creator,但现在可以直接借助 @redux/toolkit 统一的放在一个文件(slice)中,结构化的去描述 Redux 中的 action 和 redcuer。

import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { AppDispatch, RootState } from ".."; // 在 store/index.ts 中声明的类型

// 借助 createSlice 创建 reducer、action
const CounterSlice = createSlice({
name: "counter", // 生成 Action type 的前缀,例如:counter/increment
initialState: {
value: 0,
},
reducers: {
increment: (state) => {
state.value += 1; // 这里默认通过了 immer 处理,不会修改原 state
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
decrementByAmount: (state, action: PayloadAction<number>) => {
state.value -= action.payload;
},
},
});

// Action Creator 用于执行返回描述如何更新 state 的 Action
export const { increment, decrement, incrementByAmount, decrementByAmount } =
CounterSlice.actions;

// 异步 thunk,用于需要在更新数据前异步处理数据的情况
export const incrementAsync = (amount: number) => (dispatch: AppDispatch) => {
setTimeout(() => {
dispatch(incrementByAmount(amount));
}, 1500);
};

// Selector,作为 useSelector 读取数据的函数参数
export const counterSelector = (state: RootState) => state.counter.value;

// Reducer,真正执行修改 state 的纯函数
export default CounterSlice.reducer;

console.log(CounterSlice);
/*
output:
{
name: 'counter',
actions : {
increment,
decrement,
incrementByAmount,
decrementByAmount
},
reducer
}
*/

绑定 store

首先是需要将 Store 实例绑定到我们的应用上。 在 ./src/index.tsx 中添加如下:

import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux"; // 引入 Provider,绑定 store 到应用上
import store from "./store"; // 引入 store 实例
import App from "./App";
import "./index.css";

ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
{/* 绑定 store */}
<App />
</Provider>
</React.StrictMode>,
document.getElementById("root")
);

结合 react-redux 提供的 useSelector() 和 useDispatch() 可以在我们自定义的 Counter 组件中消费 counter 状态(数据)

//文件位置:src/pages/counter/index.tsx
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import {
decrement,
incrementAsync,
counterSelector,
} from "@/store/reducers/counter";
import "./index.scss";

const CounterPage = () => {
const count = useSelector(counterSelector); // 读取 count 值
const dispatch = useDispatch(); // 获得 dispatch,结合 action 就可更新 state

return (
<div className="counter-page">
<div className="counter">
{/* 同步 - */}
<div className="btn" onClick={() => dispatch(decrement())}>
-
</div>
<div className="val">{`${count}`}</div>
{/* 异步 + */}
<div className="btn" onClick={() => dispatch(incrementAsync(5))}>
+
</div>
</div>
</div>
);
};

export default CounterPage;

纵观整个案例,相较于不使用 @redux/toolkit 显著提升了研发的效率,降低了研发的使用心智负担

类型

早期 createStore

早期 createStore,直接传入 reducer,但是和 createSlice 略有不同,

createSlice 利用 immer,所以 reducer 是纯对象形式

createStore 则是含 switch case 函数

创建一个包含程序完整 state 树的 Redux store。应用中应有且仅有一个 store。

通过 createStore,传入 reducer 来创建一个 store。通过 dispatch 触发 Action,通过 getState 来获取最新的 state

import { createStore } from "redux";
// 必须存在type字段
type IAction = {
type: "ADD_TODO",
text: string,
};
// 必须是reducer类型,接收state与action,返回state
function todos(state: string = [], action: IAction) {
switch (action.type) {
case "ADD_TODO":
return state.concat([action.text]);
default:
return state;
}
}
// 第一个参数是reducer
// 第二个参数是初始值,类型被reducer的第一个参数类型限制,所以这里必须传入string[]
const store = createStore(todos, ["Use Redux"]);
// 必须传入IAction类型
store.dispatch({
type: "ADD_TODO",
text: "Read the docs",
});
// 返回state的类型
let state: string[];
let state = store.getState();
export declare function createStore<S, A extends Action, Ext, StateExt(
reducer: Reducer<S, A>,
preloadedState?: PreloadedState<S>,
enhancer?: StoreEnhancer<Ext>)
: Store<S & StateExt, A> & Ext

export type Reducer<S = any, A extends Action = Any>
Action= (
state: S | undefined,
action: A
) => S

export interface Store<S = any, A extends Action = AnyAction>{
dispatch: Dispatch<A>getState(): S
subscribe(listener: () => void): Unsubscribe
}

createStore 第一个参数类型为 Reducer,Reducer 的入参 state 与 action 的类型 S 与 A 类型几乎控制了整个 store 的类型。

第二个参数初始值 preloadedState 会被 Reducer 中 state 的类型 S 所限制。

createStore 返回是 Store 类型,也被 Reducer 的泛型 S 与 A 所约束,这也就是为什么我们使用 dispatch 以及获取 state 时,都可以拿到符合我们预期的类型。