State machines model explicit states and transitions.
The Problem: Boolean Soup
jsx// ❌ Hard to reason about function DataLoader() { const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); const [isSuccess, setIsSuccess] = useState(false); const [data, setData] = useState(null); const [error, setError] = useState(null); // What if isLoading AND isError are both true? // What states are actually valid? }
State Machine Solution
jsx// ✅ Explicit states const states = { IDLE: 'idle', LOADING: 'loading', SUCCESS: 'success', ERROR: 'error', }; function DataLoader() { const [state, setState] = useState(states.IDLE); const [data, setData] = useState(null); const [error, setError] = useState(null); // Clear which state we're in // Impossible to be loading AND error }
Core Concepts
States
Finite set of possible conditions:
idle → loading → success
↘ error
Events/Actions
Triggers that cause transitions:
- FETCH
- RESOLVE
- REJECT
- RETRY
Transitions
Rules for moving between states:
idle + FETCH → loading
loading + RESOLVE → success
loading + REJECT → error
error + RETRY → loading
Simple State Machine Pattern
jsxconst STATES = { idle: 'idle', loading: 'loading', success: 'success', error: 'error', }; const EVENTS = { FETCH: 'FETCH', RESOLVE: 'RESOLVE', REJECT: 'REJECT', RETRY: 'RETRY', }; function reducer(state, event) { switch (state.status) { case STATES.idle: if (event.type === EVENTS.FETCH) { return { status: STATES.loading }; } return state; case STATES.loading: if (event.type === EVENTS.RESOLVE) { return { status: STATES.success, data: event.data }; } if (event.type === EVENTS.REJECT) { return { status: STATES.error, error: event.error }; } return state; case STATES.success: if (event.type === EVENTS.FETCH) { return { status: STATES.loading }; } return state; case STATES.error: if (event.type === EVENTS.RETRY) { return { status: STATES.loading }; } return state; default: return state; } } function useDataFetcher(fetchFn) { const [state, dispatch] = useReducer(reducer, { status: STATES.idle }); const fetch = async () => { dispatch({ type: EVENTS.FETCH }); try { const data = await fetchFn(); dispatch({ type: EVENTS.RESOLVE, data }); } catch (error) { dispatch({ type: EVENTS.REJECT, error }); } }; const retry = () => { fetch(); }; return { state, fetch, retry }; }
Benefits of State Machines
- Impossible states are impossible
- Self-documenting logic
- Easier to test
- Predictable behavior
- Visual debugging