Skip to main content

React Integration

This guide shows how to integrate state machines with React components for predictable UI state management.

Basic React Hook Integration

Create a custom hook for state machine integration:

hooks/useStateMachine.ts
import { useState, useCallback, useRef } from 'react';
import { StateMachine, IStateMachineDefinition } from '@jewel998/state-machine';

interface UseStateMachineOptions<TContext, TState, TEvent> {
definition: IStateMachineDefinition<TContext, TState, TEvent>;
initialContext: TContext;
}

export function useStateMachine<TContext, TState, TEvent>({
definition,
initialContext,
}: UseStateMachineOptions<TContext, TState, TEvent>) {
const [currentState, setCurrentState] = useState<TState>(definition.getInitialState());
const contextRef = useRef<TContext>(initialContext);

const processEvent = useCallback(
(event: TEvent) => {
const result = definition.processEvent(currentState, event, contextRef.current);

if (result.success) {
setCurrentState(result.newState);
if (result.context) {
contextRef.current = result.context;
}
return true;
}
return false;
},
[currentState, definition]
);

const processEventAsync = useCallback(
async (event: TEvent) => {
const result = await definition.processEventAsync(currentState, event, contextRef.current);

if (result.success) {
setCurrentState(result.newState);
if (result.context) {
contextRef.current = result.context;
}
return true;
}
return false;
},
[currentState, definition]
);

const canTransition = useCallback(
(event: TEvent) => {
return definition.canTransition(currentState, event, contextRef.current);
},
[currentState, definition]
);

const getAvailableEvents = useCallback(() => {
return definition.getAvailableEvents(currentState, contextRef.current);
}, [currentState, definition]);

const reset = useCallback(() => {
setCurrentState(definition.getInitialState());
contextRef.current = initialContext;
}, [definition, initialContext]);

return {
currentState,
context: contextRef.current,
processEvent,
processEventAsync,
canTransition,
getAvailableEvents,
reset,
};
}

Form Validation Example

Create a multi-step form with state machine validation:

types/FormTypes.ts
// Define form context and types
export interface FormContext {
personalInfo: {
name: string;
email: string;
};
preferences: {
newsletter: boolean;
theme: 'light' | 'dark';
};
errors: string[];
isSubmitting: boolean;
}

export type FormState =
| 'PERSONAL_INFO'
| 'PREFERENCES'
| 'REVIEW'
| 'SUBMITTING'
| 'SUCCESS'
| 'ERROR';
export type FormEvent = 'next' | 'previous' | 'submit' | 'retry' | 'reset';

Data Fetching with State Machine

Handle complex data fetching scenarios:

types/DataTypes.ts
export interface DataContext {
data: any[];
error: string | null;
page: number;
hasMore: boolean;
retryCount: number;
}

export type DataState = 'IDLE' | 'LOADING' | 'SUCCESS' | 'ERROR' | 'LOADING_MORE';
export type DataEvent = 'fetch' | 'success' | 'error' | 'retry' | 'loadMore' | 'reset';

Context Provider Pattern

Create a context provider for sharing state machines across components:

providers/AppStateProvider.tsx
import React, { createContext, useContext, ReactNode } from 'react';
import { StateMachine } from '@jewel998/state-machine';
import { useStateMachine } from './useStateMachine';

interface AppContext {
user: { id: string; name: string } | null;
theme: 'light' | 'dark';
notifications: string[];
}

type AppState = 'LOADING' | 'AUTHENTICATED' | 'UNAUTHENTICATED';
type AppEvent = 'login' | 'logout' | 'loaded';

const appDefinition = StateMachine.definitionBuilder<AppContext, AppState, AppEvent>()
.initialState('LOADING')
.state('LOADING')
.state('AUTHENTICATED')
.state('UNAUTHENTICATED')

.transition('LOADING', 'AUTHENTICATED', 'loaded')
.guard((context) => context.user !== null)

.transition('LOADING', 'UNAUTHENTICATED', 'loaded')
.guard((context) => context.user === null)

.transition('UNAUTHENTICATED', 'AUTHENTICATED', 'login')
.transition('AUTHENTICATED', 'UNAUTHENTICATED', 'logout')
.action((context) => {
context.user = null;
})

.buildDefinition();

interface AppStateContextType {
currentState: AppState;
context: AppContext;
processEvent: (event: AppEvent) => boolean;
canTransition: (event: AppEvent) => boolean;
}

const AppStateContext = createContext<AppStateContextType | null>(null);

export const AppStateProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const stateMachine = useStateMachine({
definition: appDefinition,
initialContext: {
user: null,
theme: 'light',
notifications: [],
},
});

return (
<AppStateContext.Provider value={stateMachine}>
{children}
</AppStateContext.Provider>
);
};

export const useAppState = () => {
const context = useContext(AppStateContext);
if (!context) {
throw new Error('useAppState must be used within AppStateProvider');
}
return context;
};

// Usage in components
export const LoginButton: React.FC = () => {
const { currentState, processEvent, canTransition } = useAppState();

if (currentState === 'AUTHENTICATED') {
return (
<button onClick={() => processEvent('logout')}>
Logout
</button>
);
}

return (
<button
onClick={() => processEvent('login')}
disabled={!canTransition('login')}
>
Login
</button>
);
};

Best Practices for React Integration

1. Keep State Machines Pure

Good: Pure state machine logic
const definition = StateMachine.definitionBuilder()
.initialState('IDLE')
.transition('IDLE', 'LOADING', 'fetch')
.guard((context) => context.hasPermission) // Pure function
.buildDefinition();

2. Use Refs for Mutable Context

Use refs for context management
const useStateMachine = ({ definition, initialContext }) => {
const contextRef = useRef(initialContext);

// Always use the ref for current context
const processEvent = useCallback(
(event) => {
const result = definition.processEvent(
currentState,
event,
contextRef.current // Use ref, not state
);
// ...
},
[currentState, definition]
);
};

3. Separate UI State from Business Logic

Separate concerns properly
// Business logic in state machine
const businessDefinition = StateMachine.definitionBuilder()
.initialState('IDLE')
.transition('IDLE', 'PROCESSING', 'process')
.buildDefinition();

// UI state in React
const Component = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
const { currentState, processEvent } = useStateMachine({
definition: businessDefinition,
initialContext: {},
});

// Combine both for complete UI behavior
};

This integration provides a robust foundation for managing complex UI state with predictable behavior and excellent developer experience.