Vấn đề: Khi useState và useEffect biến thành “đống bùi nhùi”
Bạn đã bao giờ xây dựng một luồng thanh toán 5 bước với đủ loại điều kiện: kiểm tra kho, áp mã giảm giá, và tích hợp Stripe chưa? Nếu dùng useState, bạn sẽ sớm rơi vào tình trạng “Boolean Soup” — một rừng các biến isLoading, isError, isSuccess chồng chéo lên nhau.
Chỉ cần quên reset một biến flag, ứng dụng sẽ rơi vào trạng thái lỗi ngay lập tức. Theo thống kê không chính thức, hơn 70% bug giao diện đến từ việc hệ thống rơi vào các trạng thái không mong muốn (impossible states).
XState giải quyết triệt để vấn đề này bằng mô hình Finite State Machine (FSM). Hiểu đơn giản: tại một thời điểm, ứng dụng của bạn chỉ có thể ở đúng một trạng thái duy nhất. Bạn không thể vừa “đang thanh toán” vừa “chưa nhập giỏ hàng” được.
Quick start: Tạo State Machine đầu tiên trong 5 phút
Hãy thử làm một nút Toggle đơn giản nhưng được quản lý chuyên nghiệp. Trước tiên, hãy cài đặt thư viện:
npm install xstate @xstate/react
Thay vì dùng biến boolean, chúng ta định nghĩa rõ luồng đi của dữ liệu:
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' ? 'Kích hoạt' : 'Đang hoạt động'}
</button>
);
};
Cách tiếp cận này tách biệt hoàn toàn logic nghiệp vụ khỏi UI. Component của bạn bây giờ rất nhẹ nhàng. Nó chỉ việc gửi sự kiện đi và hiển thị những gì Machine trả về.
Tại sao XState lại thay đổi cuộc chơi?
1. Kiểm soát trạng thái tuyệt đối
Trong code truyền thống, việc gọi API rất dễ bị race condition. Với XState, bạn quy định rõ: từ trạng thái IDLE chỉ có thể chuyển sang LOADING. Nếu dữ liệu đang đổ về mà người dùng bấm nút Fetch lần nữa, Machine sẽ tự động bỏ qua hoặc xử lý theo kịch bản bạn đã định nghĩa trước. Không còn bất ngờ, không còn side-effect ngoài tầm kiểm soát.
2. Actor Model: Chia để trị
XState v5 đưa khái niệm Actor Model lên tầm cao mới. Hãy coi mỗi Machine là một chuyên gia riêng biệt. Một Actor lo việc thanh toán, một Actor lo việc thông báo. Chúng giao tiếp với nhau bằng tin nhắn. Cách làm này giúp bạn chia nhỏ các file logic khổng lồ thành những mẩu nhỏ dễ quản lý và dễ test độc lập.
Mô hình hóa luồng Fetch Data thực tế
Dưới đây là cách xử lý một tác vụ gọi API chuẩn chỉnh, bao gồm cả xử lý lỗi và gán dữ liệu:
import { createMachine, assign } from 'xstate';
const fetchMachine = createMachine({
id: 'fetch',
initial: 'idle',
context: { data: null, error: null },
states: {
idle: { on: { FETCH: 'loading' } },
loading: {
invoke: {
src: 'fetchData',
onDone: {
target: 'success',
actions: assign({ data: ({ event }) => event.output })
},
onError: {
target: 'failure',
actions: assign({ error: ({ event }) => event.error })
}
}
},
success: { on: { FETCH: 'loading' } },
failure: { on: { RETRY: 'loading' } }
}
});
Mọi kịch bản như “Retry khi lỗi” hay “Loading đè” đều được xử lý tập trung. Component React chỉ cần dùng state.matches('loading') để hiển thị Spinner. Cực kỳ tường minh!
Kinh nghiệm thực chiến để code “sạch” hơn
Sau nhiều dự án triển khai XState, mình rút ra 3 bí kíp giúp tối ưu quy trình làm việc:
Sử dụng Stately Visualizer
Đừng chỉ gõ code. Hãy dán logic của bạn vào Stately Viz. Công cụ này sẽ vẽ ra sơ đồ luồng chạy thực tế. Nếu sơ đồ trông như một mạng nhện rối rắm, đó là tín hiệu cho thấy bạn cần chia nhỏ Actor.
Đừng biến Context thành cái kho chứa đồ
Sai lầm phổ biến là tống tất cả biến vào context. Hãy nhớ: states để điều hướng luồng, context chỉ để chứa dữ liệu. Nếu bạn thấy mình viết quá nhiều câu lệnh if bên trong context, hãy chuyển chúng thành các trạng thái con (sub-states).
Debug JSON hiệu quả
Khi làm việc với các object lồng nhau phức tạp trong Machine, việc xem log trên console rất mệt. Mình thường copy nhanh dữ liệu vào các công cụ như JSON Formatter của Toolcraft để định dạng lại. Việc nhìn rõ cấu trúc context giúp bạn phát hiện lỗi logic nhanh hơn gấp nhiều lần.
Nâng cao: Type-safe với TypeScript
XState v5 hỗ trợ TypeScript rất mạnh. Bạn nên tận dụng để tránh truyền sai Event:
const machine = createMachine({
setup: {
types: {} as {
context: { items: string[] };
events: { type: 'ADD_ITEM'; item: string } | { type: 'CLEAR' };
},
},
// ... logic an toàn hơn nhờ auto-complete
});
Khi nào nên (và không nên) dùng XState?
XState không phải là chiếc đũa thần cho mọi dự án. Đừng dùng nó cho những form nhập liệu đơn giản hoặc các trang blog tĩnh. Tuy nhiên, hãy cân nhắc sử dụng ngay khi:
- Ứng dụng có luồng nghiệp vụ phức tạp, nhiều bước phụ thuộc nhau.
- Bạn mệt mỏi vì phải quản lý quá nhiều biến boolean
isSomething. - Team cần một sơ đồ chung (State Chart) để trao đổi giữa Dev, Designer và BA.
- Bạn muốn viết Unit Test cho logic mà không cần render UI nặng nề.
Hy vọng bài viết này giúp bạn tự tin hơn khi đối mặt với những logic “khó nhằn”. Chúc các bạn có những dòng code sạch bóng bug!

