XState状态机实战指南从入门到项目落地的完整踩坑记录

开发者利娇 框架 阅读 575
赞 23 收藏
二维码
手机扫码查看
反馈

状态管理这场仗,XState为什么能打

最近在重构一个复杂的业务流程页面,涉及到大量的状态切换和流程控制,用传统的setState搞得很混乱。于是重新拾起了XState,顺便对比了一下Redux + Redux-Saga和Zustand这两种主流方案,发现XState确实有些不一样。

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结合使用的场景,这个后面有空再分享。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论