实用类型:Redux 作为有限状态机
本文假设读者对 Redux 和类型有一定的了解,但欢迎提问。
这是本系列的第三篇文章。本文的代码在这里。
我们想做什么
我们想要创建一个表单,用户可以在其中输入数据。当用户提交表单后,我们需要在 AJAX 请求运行时显示加载状态。AJAX 请求完成后,如果成功则显示结果,如果失败则显示错误。
让我们为这项任务创建一个“经典”reducer和一个“有限状态机”reducer,以便进行比较。完整的代码在这个仓库中。
“经典”减法器
这就是“经典”reducer 的形式:
export default (reduxState: State = defaultState, action: Actions): State => {
switch (action.type) {
case "SUBMIT_FRUIT":
return {
...reduxState,
state: "fruit_loading",
form: action.form
};
case "SUBMIT_FRUIT_ERROR":
return {
...reduxState,
state: "fruit_error",
error: action.error
};
case "SUBMIT_FRUIT_OK":
return {
...reduxState,
state: "fruit_ok",
resonse: action.resonse
};
default:
exhaustiveCheck(action.type);
return reduxState;
}
};
SUBMIT_FRUIT这是响应表单提交而分发的 action。SUBMIT_FRUIT_ERROR而其他 actionSUBMIT_FRUIT_OK则会响应副作用(例如 AJAX 请求)而分发。我们可以使用不同的解决方案来处理副作用,例如 redux-thunk、redux-saga、redux-observable 或 redux-loop。但我们暂且不讨论这些,而是通过 dispatch 显式地触发副作用。
以下是 AJAX 请求的示例:
export const fruitSubmitSideEffect = (dispatch: Dispatch, form: FruitForm) => {
// uses fetch inside returns a Promise
fruitRequest(form).then(
resonse => {
dispatch({
type: "SUBMIT_FRUIT_OK",
resonse
});
},
error => {
dispatch({
type: "SUBMIT_FRUIT_ERROR",
error
});
}
);
};
// and later
export default connect(
() => ({}),
(dispatch: Dispatch) => ({
submit: (form: FruitForm) => {
dispatch({ type: "SUBMIT_FRUIT", form });
fruitSubmitSideEffect(dispatch, form);
}
})
)(Component);
之前的状态用于创建新状态,但并未显式检查:
return {
...reduxState,
...newPartsOfState
};
类型State可能如下所示:
export type State = {
state: "initial" | "fruit_loading" | "fruit_error" | "fruit_ok";
form?: FruitForm;
error?: mixed;
resonse?: FruitResponse;
};
其中一个后果是我们需要编写额外的类型检查:
export default ({ state }: { state: State }) => {
switch (state.state) {
case "fruit_ok":
return (
state.resonse && // additional type check, that it is not undefined
state.resonse.map(item => {}))
}
有限状态机
{||}有限状态机 (FSM) 应该只有有限个状态。让我们用类型系统来强制实现这一点。这是 Flow 类型,但 TypeScript 看起来也类似(在TypeScript中不需要)。
export type State =
| {|
state: "initial"
|}
| {|
state: "fruit_loading",
form: FruitForm
|}
| {|
state: "fruit_error",
form: FruitForm,
error: mixed
|}
| {|
state: "fruit_ok",
form: FruitForm,
resonse: FruitResponse
|};
现在我们不能在不进行检查的情况下使用之前的状态。如果我们这样做的话
return {
...reduxState,
state: "fruit_loading",
form: action.form
};
Flow会报错:
Could not decide which case to select. Since case 2 [1] may work but if it doesn't case 3 [2] looks promising too. To fix add a type annotation to .form [3] or to .state [3].
src/redux-fsm/state.js
[1] 12│ | {|
13│ state: "fruit_loading",
14│ form: FruitForm
15│ |}
[2] 16│ | {|
17│ state: "fruit_error",
18│ form: FruitForm,
19│ error: mixed
20│ |}
所以现在我们需要这样做:
switch (action.type) {
case "SUBMIT_FRUIT":
switch (reduxState.state) {
case "initial":
return {
state: "fruit_loading",
form: action.form
};
default:
throw new Error("Inavlid transition");
}
}
我们会检查即将发生的动作、之前的状态,然后决定下一步该怎么做。这种方法迫使我们明确地考虑系统中的所有转换。
initial
SUBMIT_FRUIT -> fruit_loading (1)
SUBMIT_FRUIT_ERROR -> ? (2)
SUBMIT_FRUIT_OK -> ? (2)
fruit_loading
SUBMIT_FRUIT -> fruit_loading (3)
SUBMIT_FRUIT_ERROR -> fruit_error (4)
SUBMIT_FRUIT_OK -> fruit_ok (5)
fruit_error
SUBMIT_FRUIT -> fruit_loading (6)
SUBMIT_FRUIT_ERROR -> ? (7)
SUBMIT_FRUIT_OK -> ? (7)
fruit_ok
SUBMIT_FRUIT -> fruit_loading (6)
SUBMIT_FRUIT_ERROR -> ? (7)
SUBMIT_FRUIT_OK -> ? (7)
附注:为什么要这样做?为了正式定义用户界面,证明用户界面逻辑中没有错误。例如:
- 您可以使用sketch.systems来创建 UI 逻辑原型。
- 使用 Alloy(比 TLA+ 更轻量级的替代方案)来分析您的 UI
- 这份规范可以在用户体验人员和开发人员之间共享。
- 另请参阅“验证 ReasonReact 组件逻辑 — ReasonML 和 Imandra”。
附注 2:我在 reducer 中实现了“反向”有限状态机,它先检查 action,再检查 state。
(1,5)“正常”路径 - 用户提交表单并收到响应。
(1,4)错误路径 - 用户提交表单并收到错误。
(6)重复搜索 - 已有错误或成功响应,用户重复搜索。
(2)永远不会发生 - 我们可以假设这种情况永远不会发生,并在这种情况下抛出异常。(
7)竞态条件 - 我们已经有一个响应(或错误),但又收到一个新的响应,这种情况只有在我们允许同时存在多个副作用时才会发生。
(3)重复搜索 - 有一个搜索正在处理中,但用户请求不同的搜索,或者可能不耐烦地点击。这是一个有趣的情况。我们应该怎么做?我们可以:
- 忽略它(也可以通过禁用按钮的视觉效果来传达此信息)。
- 取消之前的请求并发起新的请求。
- 启动一个新的进程,并忽略之前的进程。这基本上就是我们在“传统”方法中所做的,但这也会导致情况 (7),即竞态条件。此外,这种方法在 (1, 5) 和 (1, 4) 场景中也会引入竞态条件。
对于这篇文章,我选择忽略它,因为这是最简单的解决方案,也许我会在下一篇文章中实现取消功能。
这就是为什么要使用有限状态机(FSM)的原因,这种方法有助于发现逻辑中的“漏洞”。系统中的状态越多,隐藏的潜在漏洞就越多。
如果你觉得查找这类漏洞太麻烦,想想IT支持人员常问的问题:“你试过重启吗?”。没错,某个地方肯定隐藏着与系统状态相关的漏洞,而解决办法就是重启系统,将状态重置为初始状态。
另一方面,我也同意 JS(或者 Flow 或 TS)的语法对于这类任务来说有点笨拙。使用 switch 语句进行模式匹配不够简洁。Redux 需要的样板代码比传统方法更多。您怎么看?如果它需要的样板代码更少,您会使用它吗?
照片由 Dan Lohmar 在 Unsplash 上拍摄
本文是系列文章之一。请在推特和GitHub上关注我。
文章来源:https://dev.to/stereobooster/pragmatic-types-how-to-turn-redux-to-finite-state-machine-with-the-help-of-types-5f08