Vượt qua “Boolean Soup”: Kiến trúc Logic React mạnh mẽ với XState

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

Vấn đề với “Boolean Soup”

Các nhà phát triển React thường có thói quen quản lý logic component bằng một tập hợp các hook useState rời rạc. Có thể bạn đã từng viết hoặc bắt gặp những đoạn code trông như thế này:

const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [data, setData] = useState(null);
const [isSuccess, setIsSuccess] = useState(false);

Nhìn bề ngoài, cách này có vẻ ổn. Tuy nhiên, nó tạo ra không gian cho các “trạng thái không khả thi” (impossible states), nơi isLoadingisSuccess về mặt kỹ thuật có thể đồng thời là true. Những xung đột logic này chính là nơi nảy sinh các lỗi UI gây ức chế nhất. Trong một dự án gần đây khi chuyển đổi luồng thanh toán cũ, việc chuyển từ boolean sang state machine đã giúp chúng tôi giảm gần 60% các báo cáo lỗi liên quan đến trạng thái.

Bằng cách áp dụng một state machine, bạn xác định chính xác những trạng thái nào là hợp lệ. Bạn quy định cụ thể cách ứng dụng chuyển đổi từ điểm này sang điểm tiếp theo, không để lại kẽ hở cho các sự chồng chéo ngoài ý muốn.

Bắt đầu nhanh: Machine đầu tiên của bạn

Hãy cùng xây dựng một nút gạt (toggle switch) chức năng. Trước tiên, hãy cài đặt các gói cần thiết:

npm install xstate @xstate/react

Thay vì dùng các biến boolean thủ công, chúng ta định nghĩa một machine. Hãy coi đây là một bản thiết kế cho hành vi của component, bao gồm các states (trạng thái) và events (sự kiện).

import { createMachine } from 'xstate';
import { useMachine } from '@xstate/react';

const toggleMachine = createMachine({
  id: 'toggle',
  initial: 'inactive',
  states: {
    inactive: {
      on: { TOGGLE: 'active' }
    },
    active: {
      on: { TOGGLE: 'inactive' }
    }
  }
});

export const Toggle = () => {
  const [state, send] = useMachine(toggleMachine);

  return (
    <button onClick={() => send({ type: 'TOGGLE' })}>
      {state.value === 'inactive' ? 'Nhấn để kích hoạt' : 'Đang hoạt động!'}
    </button>
  );
};

Machine chỉ tồn tại ở duy nhất một trạng thái: inactive hoặc active. Khi sự kiện TOGGLE được kích hoạt, nó sẽ chuyển sang trạng thái còn lại. Việc ở cả hai trạng thái hoặc không ở trạng thái nào là điều không thể xảy ra về mặt vật lý, tạo nên một nền tảng vững chắc cho UI của bạn.

Tại sao State Machine lại thay đổi cuộc chơi

Một state machine không chỉ là một object cầu kỳ; nó là một mô hình toán học cho logic của bạn. Trong phát triển UI, nó ánh xạ mọi “chế độ” (mode) khả thi mà component của bạn có thể có. XState nâng tầm điều này bằng cách triển khai Statecharts. Chúng cho phép thực hiện các mô hình phức tạp như trạng thái phân cấp (nested states), logic song song và lịch sử giống như bộ nhớ.

Loại bỏ các trạng thái không khả thi

Hãy xem xét một trình tải lên video. Logic thường bao gồm idle (chờ), selecting (đang chọn), uploading (đang tải lên), success (thành công) và error (lỗi). Nếu dựa vào các biến boolean, người dùng có thể kích hoạt hộp thoại “Chọn tệp” trong khi quá trình tải lên đã hoàn thành 80%. XState ngăn chặn điều này bằng cách đơn giản là không định nghĩa bước chuyển đổi SELECT_FILE khi đang ở trạng thái uploading. UI của bạn trở thành một phản chiếu trực tiếp và có thể dự đoán được của logic.

Trực quan hóa logic trước khi viết code

Sức mạnh thực sự của XState nằm ở khả năng trực quan hóa. Bạn có thể dán machine của mình vào Stately Visualizer để tạo một lưu đồ trực tiếp. Điều này xóa nhòa khoảng cách giữa kỹ thuật và thiết kế sản phẩm. Thay vì giải thích 200 dòng code cho các bên liên quan, bạn chỉ cần cho họ xem sơ đồ trực quan về logic nghiệp vụ.

Sử dụng nâng cao: Dữ liệu và Side Effects

Các ứng dụng thực tế cần làm nhiều việc hơn là chỉ bật tắt nút; chúng cần lấy dữ liệu. XState quản lý việc này thông qua Context (bộ nhớ trong) và Actors (logic bất đồng bộ).

const fetchMachine = createMachine({
  id: 'fetch',
  initial: 'idle',
  context: { data: null, error: null },
  states: {
    idle: {
      on: { FETCH: 'loading' }
    },
    loading: {
      invoke: {
        src: 'getUser',
        onDone: {
          target: 'success',
          actions: assign({ data: ({ event }) => event.output })
        },
        onError: {
          target: 'failure',
          actions: assign({ error: ({ event }) => event.error })
        }
      }
    },
    success: {},
    failure: {
      on: { RETRY: 'loading' }
    }
  }
});

Sử dụng invoke cho phép XState quản lý vòng đời của một Promise. Nó tự động xử lý các bước chuyển sang success hoặc failure dựa trên kết quả. Hàm assign cập nhật context, đóng vai trò như bộ nhớ trong của machine.

Guard và Action

  • Guards: Đóng vai trò như các cổng điều kiện. Ví dụ, bạn có thể ngăn chặn chuyển đổi “Submit” trừ khi bước kiểm tra formIsValid thành công.
  • Actions: Sử dụng cho các side effect kiểu “chạy và quên”. Chúng hoàn hảo để kích hoạt thông báo (toast notification) hoặc ghi log analytics khi một trạng thái cụ thể được bắt đầu.

Mẹo triển khai thực tế

Chuyển sang tư duy ưu tiên trạng thái (state-first) cần có thời gian. Dưới đây là cách tích hợp XState vào các ứng dụng React thực tế một cách hiệu quả:

Bắt đầu với những phần có độ phức tạp cao

Tránh việc bao bọc toàn bộ ứng dụng trong một machine khổng lồ duy nhất. Hãy tập trung vào các component nơi việc quản lý trạng thái thường dễ bị sụp đổ, chẳng hạn như biểu mẫu thanh toán nhiều bước, luồng xác thực phức tạp hoặc các dashboard nhiều dữ liệu.

Tận dụng Inspector

Sử dụng gói @xstate/inspect trong quá trình phát triển. Nó mở ra một cửa sổ trình duyệt riêng biệt hiển thị trạng thái hiện tại và lịch sử sự kiện của machine theo thời gian thực. Nó mang lại mức độ minh bạch mà các dòng console log thông thường không thể sánh được.

Biết khi nào nên giữ mọi thứ đơn giản

XState là quá mức cần thiết cho một ô nhập văn bản đơn giản hoặc hiệu ứng hover cơ bản. Nếu logic của bạn chỉ bao gồm một hoặc hai trạng thái, useState là lựa chọn tốt nhất. Hãy dành XState cho các logic có từ ba trạng thái trở lên hoặc những logic có các bước chuyển đổi phức tạp, không tuyến tính.

Kiểm thử độc lập

Vì logic của bạn nằm trong một machine thay vì trong component, bạn có thể unit test nó mà không cần render bất kỳ pixel nào. Bạn có thể gửi các sự kiện đến machine và kiểm tra trạng thái cuối cùng. Điều này giúp việc kiểm thử các trường hợp biên (edge cases) — như lỗi mạng trong một bước cụ thể — trở nên nhanh chóng và đáng tin cậy hơn đáng kể.

Đầu tư vào state machine ban đầu có thể cảm thấy như một gánh nặng thêm. Tuy nhiên, sự rõ ràng và khả năng giảm thiểu lỗi mà nó mang lại cho một codebase đang phát triển là vô giá. Một khi bạn đã trải nghiệm tính dễ dự đoán của XState, việc quay lại với “boolean soup” sẽ mang lại cảm giác như một bước lùi.

Share: