XState状态机实战指南从入门到项目落地的完整踩坑记录
状态管理这场仗,XState为什么能打
最近在重构一个复杂的业务流程页面,涉及到大量的状态切换和流程控制,用传统的setState搞得很混乱。于是重新拾起了XState,顺便对比了一下Redux + Redux-Saga和Zustand这两种主流方案,发现XState确实有些不一样。
我比较喜欢用XState的主要原因是它把状态变化的逻辑给固化了,不像Redux那样需要各种action来回dispatch,也不像Zustand那样状态散落一地。这次就来聊聊我实际用下来的感受。
三套方案的代码长这样
先看看各自的代码风格吧。以一个简单的用户登录状态为例:
XState版本,定义一个machine:
import { createMachine, assign } from 'xstate';
const authMachine = createMachine({
id: 'auth',
initial: 'idle',
context: {
user: null,
error: null
},
states: {
idle: {
on: {
LOGIN: 'loggingIn'
}
},
loggingIn: {
invoke: {
src: 'login',
onDone: {
target: 'authenticated',
actions: assign({
user: (context, event) => event.data.user
})
},
onError: {
target: 'unauthenticated',
actions: assign({
error: (context, event) => event.data.error
})
}
}
},
authenticated: {
on: {
LOGOUT: 'unauthenticated'
}
},
unauthenticated: {
on: {
LOGIN: 'loggingIn'
}
}
}
});
Redux + Saga的写法就比较分散了,action types一堆:
// actions.js
export const LOGIN_REQUEST = 'LOGIN_REQUEST';
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
export const LOGIN_FAILURE = 'LOGIN_FAILURE';
export const loginRequest = (credentials) => ({
type: LOGIN_REQUEST,
payload: credentials
});
// reducer.js
const initialState = {
user: null,
loading: false,
error: null
};
export default function authReducer(state = initialState, action) {
switch (action.type) {
case LOGIN_REQUEST:
return { ...state, loading: true, error: null };
case LOGIN_SUCCESS:
return {
...state,
loading: false,
user: action.payload.user,
error: null
};
case LOGIN_FAILURE:
return {
...state,
loading: false,
error: action.payload.error
};
default:
return state;
}
}
// saga.js
function* loginSaga(action) {
try {
const response = yield call(api.login, action.payload);
yield put({ type: LOGIN_SUCCESS, payload: response });
} catch (error) {
yield put({ type: LOGIN_FAILURE, payload: error.message });
}
}
Zustand就简洁多了,但是状态管理的逻辑散落在各个函数里:
import { create } from 'zustand';
const useAuthStore = create((set) => ({
user: null,
loading: false,
error: null,
login: async (credentials) => {
set({ loading: true, error: null });
try {
const response = await api.login(credentials);
set({ user: response.user, loading: false });
} catch (error) {
set({ error: error.message, loading: false });
}
},
logout: () => {
set({ user: null });
}
}));
谁更灵活?谁更省事?
从开发效率来说,Zustand肯定是最快的,几行代码搞定。Redux需要写一堆样板代码,虽然TypeScript配合Redux Toolkit能减少不少工作量,但还是绕不过那些概念。
XState的学习曲线是最陡的,FSM的概念需要时间理解。但我发现一旦习惯了这种思维模式,复杂的状态逻辑反而变得清晰了。特别是那种有多层嵌套状态的场景,比如表单填写过程中还有子步骤的状态变化,XState的可视化调试工具简直是神器。
这里注意我踩过好几次坑,就是一开始没想清楚状态机的设计,后面改起来特别痛苦。XState最适合那种状态转换逻辑比较固定的场景,比如订单流程、审批流程这种。
Redux的优势在于生态成熟,调试工具完善,团队协作容易。但是复杂的异步流程处理起来确实比较麻烦,saga虽然能解决,但增加了复杂度。
Zustand适合中小型应用,简单直接,但是当状态逻辑变得复杂时,很容易出现状态不一致的问题。
性能和维护性考量
性能方面其实差别不大,现代框架的更新机制都做得不错。我主要考虑的是维护成本。
用XState的时候,我一般会先画个状态转换图,然后按照图来写machine。这样做出来的代码基本不会有逻辑错误,因为状态转换路径都被明确定义了。而且同事接手的时候,看状态机比看一堆dispatch要直观得多。
Redux虽然样板代码多,但是状态流向很清楚,trace bug相对容易。Zustand的问题是状态变更的地方太多,有时候不知道数据是从哪里变的。
从TypeScript支持角度看,XState的类型推导确实厉害,大部分情况都不用手动声明类型。Redux配合TS也还行,就是配置稍微复杂。Zustand的类型支持也不错,但不如XState那么智能。
我的选型逻辑
简单组件级别的状态,我会直接用React hooks + zustand,不用整那么复杂的东西。
中大型应用的状态管理,如果状态逻辑相对简单,我会选Redux或者Zustand,毕竟团队成员熟悉度高。
但如果涉及到复杂的业务流程,特别是那种需要流程回溯、状态恢复的场景,我比较喜欢用XState。之前做过一个在线教育的课程播放器,包括暂停、重试、跳转、保存进度这些状态,用XState处理起来就很舒服。
另外XState的可视化工具对于调试复杂状态机很有帮助,jztheme.com上有一些不错的状态机设计案例可以参考。
以上是我的对比总结,有不同看法欢迎评论区交流
总的来说,这三种方案各有优劣,没有绝对的好坏。我的经验是根据具体的业务场景来选择,复杂状态逻辑选XState,简单应用选Zustand,团队项目选Redux。当然,最终还是要看团队的技术栈和熟悉程度。
最近还在探索XState的更多用法,特别是跟React Query或者SWR结合使用的场景,这个后面有空再分享。

暂无评论