TypeScript + React 最佳实践--第 2 节:Redux 类型 (1)
前言
TypeScript + React 类型安全三件套:Component、Redux、和Service类型化。
上一节的 Component 类型化里有两个 TODO,mapStateToProps(state: IRootState)
的 IRootState
和 DispatchProps
里 actions
,这两个类型是来自 Redux,故而本节的主要内容是 Redux 类型化。
Redux类型化
涉及以下几个子集:
- state 类型化
- actions 类型化
- action & reducer 类型化
state 类型化
可以将所有子 state 的类型合并到一起,得到 IRootState
:
interface IStateA {}
interface IStateB {}
...
type IRootState = IStateA & IStateA & ...;
也可以将所有子 state 的值合并到一起,推断得到 IRootState
:
interface IStateA {}
const stateA: IStateA = {}
interface IStateB {}
const stateB: IStateB = {}
...
const rootState = { ...stateA, ...stateB, ... };
type IRootState = typeof rootState;
actions 类型化
同样,可以将相关的 action 值合并到一起——这里,如果单个 action* 是类型化的,则 actions 也是类型化的:
const actions = {
doA,
doB,
doC,
...
};
export default actions;
action & reducer 类型化
我们可以引入 typesafe-actions
和 redux-actions
来实现这个目的。
npm i typesafe-actions redux-actions;
创建类型化的 action
redux-actions
也提供了一个 createAction
接口,由于TS类型推断机制,会造成 action 的 payload 类型错误,因此选择 typesafe-actions
:
import { createAction } from 'typesafe-actions';
const DOA = 'DOA';
interface IParamsA { ... }
const doA = (paramsA: IParamsA) => createAction(DOA, paramsA);
创建类型化的 reducer
我们可以引入 redux-actions
的 handleActions
来创建 reducer, 以避免写 switch case:
const reducerA = handleActions(
{
[doA]: (state: IStateA, action: ReturnType<typeof doA>) => {
return {
...state,
...action.payload
};
}
},
stateA
)
这样似乎就可以了——但是真的很繁琐:
- 传统的state & action Type & action & reducer 1v1v1v1,写起来太繁琐了。
- 合并所有子 state,子 action,维护起来就更繁琐了。
Model
所幸传统 redux 繁琐的问题,已经早有人意识到并且实现了解决方案,这就是 dvajs:
dvajs model
dvajs 提供了一种清晰的组织 state、actions & reducers、effects 的数据结构,示例:
app.model({
namespace: 'xxx',
state: [],
reducers: {
doSomething(state, action) => {}
},
effects: {
*doSomethingAsync(action, utils){}
}
})
结构里 reducers 和 effects 内的方法会被转换为同名的 action, 并通过 ${namespace}/${keyName}
取值的 action type 将 action 和 reducer 自动关联起来。
类型化 model
基于 dvajs 的数据结构设计,有了 tkit-model——类型化的 model。
引入依赖:
npm i tkit-model
后续会迁移到 @tefe/model
下,并可以通过 @tefe/redux-model
和 @tefe/use-model
分别引用 redux model 和 use-model hooks。
示例:
// userModel.ts
import createModel, { CM } from 'tkit-model';
interface State { data: number[] }
const state: State = { data: [] };
const userModel = CM({
namespace: 'user',
state
reducer: {
doSomething: (state, action: { payload: number }) {
// ok
state.data.push(action.payload);
// type error
state.data.push('类型错误');
}
},
effects: {
*doSomethingAsync({ tPut }, action: { paylong: number }) {
// ok
yield tPut(model.actions.doSomething, action.payload);
// type error
yield tPut(model.actions.doSomething, '类型错误');
}
}
});
const { userModelState, reducers, actions, sagas } = userModel;
将 model 的 state、reducers、actions、sagas 注册到对应的全局 redux 上。
// component.tsx
type DispatchProps = ReturnType<typeof mapDispatchToProps>;
type Props = OwnProps & DispatchProps;
function Cp(props: Props) {
return (
<>
<button onclick={() => props.actions.doSomethingAsync(1)}>click</button>
<button onclick={() => props.actions.doSomethingAsync('类型错误')}>click</button>
</>
)
}
function mapDispatchToProps(dispatch: Dispatch) {
return {
actions: bindActionCreators(actions, dispatch)
};
}
TODO
自动合并各子 actions、state、reducers等物料的 CLI,解决合并繁琐的问题。
上一篇: 多模态是后 GPT 时代的最大机遇
下一篇: 历史舞台上的人类--新生代地球史