Mục lục

Quản lý Flow phức tạp với State Machine

Giải quyết bài toán quy trình duyệt nhiều bước (Multi-step Approval) bằng Finite State Machine (FSM). Tránh 'boolean hell' và quản lý logic chuyển trạng thái chặt chẽ.

1. Vấn đề: "Boolean Hell" trong quy trình nhiều bước

Bạn đang xây dựng chức năng "Đăng bán sản phẩm" với quy trình kiểm duyệt gắt gao:

  1. Draft: Người bán nhập thông tin.
  2. Reviewing: Admin kiểm tra nội dung.
  3. Pricing: Tài chính duyệt giá.
  4. Approved: Sẵn sàng niêm yết.
  5. Published: Đã lên sàn.
  6. Rejected: Bị từ chối (quay lại Draft).

Nếu dùng useStateif/else, bạn sẽ gặp ác mộng:

tsx:
// ❌ CÁCH LÀM SAI: Rối rắm, khó bảo trì
if (status === 'DRAFT' && hasInfo) {
  setShowSubmitReview(true);
} else if (status === 'REVIEWING') {
   if (role === 'ADMIN') setShowApprove(true);
}
// Và hàng tá biến flag: isPricingLocked, canPublish, isRejected...

2. Giải pháp: Finite State Machine (FSM)

Thay vì quản lý các biến boolean rời rạc, ta coi toàn bộ quy trình là một Cỗ máy trạng thái (Machine). Tại một thời điểm, hệ thống chỉ ở đúng 1 Trạng thái (State). Nó chỉ có thể chuyển sang trạng thái khác thông qua một Sự kiện (Event) cụ thể.

Sơ đồ tư duy (Conceptual Model)


3. Implementation (Dùng useReducer Pattern)

Chúng ta không cần thư viện nặng như XState cho các cases đơn giản/trung bình. useReducer là đủ.

Bước 1: Định nghĩa States & Events

typescript:
// domain/product-flow.types.ts

export type ProductState = 
  | 'DRAFT' 
  | 'REVIEWING' 
  | 'PRICING_CHECK' 
  | 'APPROVED' 
  | 'PUBLISHED' 
  | 'REJECTED';

export type ProductEvent = 
  | { type: 'SUBMIT_FOR_REVIEW' }
  | { type: 'ADMIN_APPROVE_CONTENT' }
  | { type: 'ADMIN_APPROVE_PRICE' }
  | { type: 'REJECT'; reason: string }
  | { type: 'PUBLISH' }
  | { type: 'RESET_TO_DRAFT' };

export interface ProductContext {
  id: string;
  data: any;
  rejectionReason?: string;
  stepsCompleted: {
    info: boolean;
    pricing: boolean;
  };
}

Bước 2: Viết Reducer (The Machine Core)

Đây là nơi chứa toàn bộ logic chuyển đổi. "Nếu đang ở A, nhận sự kiện B, thì sang C".

typescript:
// domain/product-flow.machine.ts

export function productFlowReducer(
  state: ProductState, 
  event: ProductEvent
): ProductState {
  switch (state) {
    case 'DRAFT':
      if (event.type === 'SUBMIT_FOR_REVIEW') return 'REVIEWING';
      return state;

    case 'REVIEWING':
      if (event.type === 'ADMIN_APPROVE_CONTENT') return 'PRICING_CHECK';
      if (event.type === 'REJECT') return 'REJECTED';
      return state;

    case 'PRICING_CHECK':
      if (event.type === 'ADMIN_APPROVE_PRICE') return 'APPROVED';
      if (event.type === 'REJECT') return 'REJECTED';
      // Nếu content sai, có thể quay lại bước trước? Tùy logic.
      return state;

    case 'APPROVED':
      if (event.type === 'PUBLISH') return 'PUBLISHED';
      return state;

    case 'REJECTED':
      if (event.type === 'RESET_TO_DRAFT') return 'DRAFT';
      return state;

    default:
      return state;
  }
}

Bước 3: Custom Hook + Guards (Presenter Layer)

Kết hợp State Machine với Business Rules (Guards) để đảm bảo tính chặt chẽ.

typescript:
// hooks/useProductWorkflow.ts
import { useReducer } from 'react';

export function useProductWorkflow(initialState: ProductState = 'DRAFT', context: ProductContext) {
  const [state, dispatch] = useReducer(productFlowReducer, initialState);

  // --- GUARDS (Điều kiện chặn) ---
  // Step 2 bị khoá nếu Step 1 chưa xong
  const canSubmit = state === 'DRAFT' && context.stepsCompleted.info && context.stepsCompleted.pricing;
  
  const canPublish = state === 'APPROVED';

  // --- ACTIONS ---
  const submit = () => {
    if (!canSubmit) return; // Guard check
    // Có thể gọi API update status ở đây
    dispatch({ type: 'SUBMIT_FOR_REVIEW' });
  };

  const adminReject = (reason: string) => {
    dispatch({ type: 'REJECT', reason });
  };

  return {
    state,
    isStepLocked: {
      review: state === 'DRAFT', // Review bị khóa khi đang Draft
      publish: state !== 'APPROVED'
    },
    actions: {
      submit,
      adminReject,
      // ...
    }
  };
}

3.5. Minh họa cơ chế hoạt động (Sequence Diagram)


4. UI Integration (Clean & Declarative)

UI bây giờ rất "ngu" (dumb), chỉ hiển thị dựa trên trạng thái trả về từ hook.

tsx:
// components/ProcessBar.tsx
const ProcessBar = ({ workflow }) => {
  const { state, actions, isStepLocked } = workflow;

  return (
    <div className="flex gap-4">
      {/* Step 1 */}
      <Step status={state === 'DRAFT' ? 'active' : 'completed'}>
        Nhập liệu
      </Step>

      {/* Step 2 (Có logic lock) */}
      <Step 
        status={state === 'REVIEWING' ? 'active' : 'pending'} 
        locked={isStepLocked.review}
      >
        Xét duyệt nội dung
      </Step>

      {/* Button Action thay đổi theo State */}
      {state === 'DRAFT' && (
        <Button onClick={actions.submit}>Gửi duyệt</Button>
      )}
      
      {state === 'REVIEWING' && (
         <Button variant="danger" onClick={() => actions.adminReject("Sai giá")}>
           Từ chối
         </Button>
      )}
    </div>
  );
};

5. Khi nào dùng pattern này?

PatternThích hợp cho
Simple State (useState)Form đơn giản, Toggle Modal, Input field.
Reducer (useReducer)Form nhiều trường, logic phụ thuộc lẫn nhau ít.
State Machine (Pattern này)Quy trình (Workflow), Wizard nhiều bước, Game logic, Payment Flow.
XState (Thư viện)Flow cực kỳ phức tạp, cần visualize graph, nested states, parallel states.

Lợi ích cốt lõi

  1. Impossible States Impossible: Không bao giờ có chuyện "Đang Approved mà lại hiện nút Gửi duyệt". Machine chặn ngay từ root.
  2. Dễ Test: Bạn chỉ cần test hàm productFlowReducer thuần túy (Unit Test) mà không cần render UI.
  3. Tư duy rõ ràng: Nhìn vào sơ đồ trạng thái là hiểu ngay nghiệp vụ, không cần đọc code lung tung.

6. Nâng cao: Sử dụng XState (Khi flow siêu phức tạp)

Khi logic bắt đầu có "Trạng thái con" (Nested Steps) hoặc "Thời gian" (sau 3 ngày tự hủy), useReducer sẽ bắt đầu đuối. Lúc này XState là lựa chọn số 1.

Ví dụ: XState Machine Definition

typescript:
import { createMachine } from 'xstate';

export const productMachine = createMachine({
  id: 'product',
  initial: 'draft',
  states: {
    draft: {
      on: { SUBMIT: 'reviewing' }
    },
    reviewing: {
      initial: 'checkingContent', // Nested State
      states: {
        checkingContent: {
          on: { 
            APPROVE: 'checkingPrice',
            REJECT: '#product.rejected' // Go to global rejected
          }
        },
        checkingPrice: {
          on: { 
            APPROVE: '#product.approved',
            REJECT: '#product.rejected'
          }
        }
      }
    },
    approved: {
      // Auto publish sau 3 ngày (Tính năng XState rất mạnh)
      after: {
        259200000: 'published'
      },
      on: { PUBLISH: 'published' }
    },
    published: {
      type: 'final'
    },
    rejected: {
      on: { EDIT: 'draft' }
    }
  }
});

Sử dụng trong React

tsx:
import { useMachine } from '@xstate/react';
import { productMachine } from './productMachine';

const ProductWorkflow = () => {
  const [state, send] = useMachine(productMachine);

  return (
    <div>
      {state.matches('reviewing.checkingContent') && <p>Đang duyệt nội dung...</p>}
      
      {state.matches('draft') && (
        <button onClick={() => send({ type: 'SUBMIT' })}>Gửi duyệt</button>
      )}
    </div>
  );
};
Quảng cáo
mdhorizontal