We use cookies to enhance your experience on the site
CodeWorlds

State Management Patterns in JavaScript/TypeScript

Just as ancient kings had to manage their kingdoms using proven administration systems, modern applications need effective state management patterns. In this lesson, we will learn the most important patterns: Flux, Event Sourcing, and State Machines, and learn how to implement them in JavaScript/TypeScript for different application scenarios.

Flux Architecture - Unidirectional data flow

Flux is an application architecture pattern that introduces unidirectional data flow, making application state more predictable and easier to debug.

Basic Flux components

1// Types for Flux
2type Action<T = any> = {
3  type: string;
4  payload?: T;
5};
6
7type Listener = () => void;
8
9// Store - holds application state
10class Store<T> {
11  private state: T;
12  private listeners: Listener[] = [];
13
14  constructor(initialState: T) {
15    this.state = initialState;
16  }
17
18  getState(): T {
19    return this.state;
20  }
21
22  protected setState(newState: T): void {
23    this.state = newState;
24    this.emitChange();
25  }
26
27  subscribe(listener: Listener): () => void {
28    this.listeners.push(listener);
29
30    // Return unsubscribe function
31    return () => {
32      const index = this.listeners.indexOf(listener);
33      if (index > -1) {
34        this.listeners.splice(index, 1);
35      }
36    };
37  }
38
39  private emitChange(): void {
40    this.listeners.forEach(listener => listener());
41  }
42}
43
44// Dispatcher - central hub for all actions
45class Dispatcher {
46  private callbacks: Array<(action: Action) => void> = [];
47
48  register(callback: (action: Action) => void): string {
49    const id = Math.random().toString(36).substr(2, 9);
50    this.callbacks.push(callback);
51    return id;
52  }
53
54  dispatch(action: Action): void {
55    console.log('Dispatching action:', action);
56    this.callbacks.forEach(callback => {
57      callback(action);
58    });
59  }
60}
61
62// Action Creators - functions that create actions
63const TodoActions = {
64  addTodo: (text: string): Action<{ text: string }> => ({
65    type: 'ADD_TODO',
66    payload: { text }
67  }),
68
69  toggleTodo: (id: string): Action<{ id: string }> => ({
70    type: 'TOGGLE_TODO',
71    payload: { id }
72  }),
73
74  deleteTodo: (id: string): Action<{ id: string }> => ({
75    type: 'DELETE_TODO',
76    payload: { id }
77  }),
78
79  setFilter: (filter: 'all' | 'active' | 'completed'): Action<{ filter: string }> => ({
80    type: 'SET_FILTER',
81    payload: { filter }
82  })
83};
84
85// Application state
86interface Todo {
87  id: string;
88  text: string;
89  completed: boolean;
90  createdAt: Date;
91}
92
93interface TodoState {
94  todos: Todo[];
95  filter: 'all' | 'active' | 'completed';
96}
97
98// TodoStore implementation
99class TodoStore extends Store<TodoState> {
100  constructor(dispatcher: Dispatcher) {
101    super({
102      todos: [],
103      filter: 'all'
104    });
105
106    // Register with dispatcher
107    dispatcher.register((action: Action) => {
108      this.handleAction(action);
109    });
110  }
111
112  private handleAction(action: Action): void {
113    const currentState = this.getState();
114
115    switch (action.type) {
116      case 'ADD_TODO':
117        this.setState({
118          ...currentState,
119          todos: [
120            ...currentState.todos,
121            {
122              id: Math.random().toString(36).substr(2, 9),
123              text: action.payload.text,
124              completed: false,
125              createdAt: new Date()
126            }
127          ]
128        });
129        break;
130
131      case 'TOGGLE_TODO':
132        this.setState({
133          ...currentState,
134          todos: currentState.todos.map(todo =>
135            todo.id === action.payload.id
136              ? { ...todo, completed: !todo.completed }
137              : todo
138          )
139        });
140        break;
141
142      case 'DELETE_TODO':
143        this.setState({
144          ...currentState,
145          todos: currentState.todos.filter(todo => todo.id !== action.payload.id)
146        });
147        break;
148
149      case 'SET_FILTER':
150        this.setState({
151          ...currentState,
152          filter: action.payload.filter
153        });
154        break;
155
156      default:
157        // No state changes for unknown actions
158        break;
159    }
160  }
161
162  // Selectors - functions for retrieving processed data
163  getFilteredTodos(): Todo[] {
164    const { todos, filter } = this.getState();
165
166    switch (filter) {
167      case 'active':
168        return todos.filter(todo => !todo.completed);
169      case 'completed':
170        return todos.filter(todo => todo.completed);
171      default:
172        return todos;
173    }
174  }
175
176  getStats() {
177    const todos = this.getState().todos;
178    return {
179      total: todos.length,
180      completed: todos.filter(todo => todo.completed).length,
181      active: todos.filter(todo => !todo.completed).length
182    };
183  }
184}
185
186// Using Flux
187const dispatcher = new Dispatcher();
188const todoStore = new TodoStore(dispatcher);
189
190// Subscribing to changes
191const unsubscribe = todoStore.subscribe(() => {
192  console.log('State changed:', {
193    todos: todoStore.getFilteredTodos(),
194    stats: todoStore.getStats(),
195    filter: todoStore.getState().filter
196  });
197});
198
199// Executing actions
200dispatcher.dispatch(TodoActions.addTodo('Learn Flux'));
201dispatcher.dispatch(TodoActions.addTodo('Implement TodoApp'));
202dispatcher.dispatch(TodoActions.toggleTodo(todoStore.getState().todos[0].id));
203dispatcher.dispatch(TodoActions.setFilter('active'));

Advanced Flux with Middleware

1// Middleware for Flux
2type Middleware = (action: Action, next: (action: Action) => void) => void;
3
4class EnhancedDispatcher extends Dispatcher {
5  private middlewares: Middleware[] = [];
6
7  use(middleware: Middleware): void {
8    this.middlewares.push(middleware);
9  }
10
11  dispatch(action: Action): void {
12    let index = 0;
13
14    const next = (currentAction: Action) => {
15      if (index < this.middlewares.length) {
16        const middleware = this.middlewares[index++];
17        middleware(currentAction, next);
18      } else {
19        super.dispatch(currentAction);
20      }
21    };
22
23    next(action);
24  }
25}
26
27// Logging middleware
28const loggingMiddleware: Middleware = (action, next) => {
29  console.group(\`Action: \${action.type}\`);
30  console.log('Payload:', action.payload);
31  console.time('Action Duration');
32
33  next(action);
34
35  console.timeEnd('Action Duration');
36  console.groupEnd();
37};
38
39// Debug middleware
40const debugMiddleware: Middleware = (action, next) => {
41  if (process.env.NODE_ENV === 'development') {
42    console.log('Debug: Before action', action);
43  }
44
45  next(action);
46
47  if (process.env.NODE_ENV === 'development') {
48    console.log('Debug: After action', action);
49  }
50};
51
52// Async action handling middleware
53const asyncMiddleware: Middleware = (action, next) => {
54  if (typeof action.payload === 'function') {
55    // This is an async action
56    action.payload((asyncAction: Action) => {
57      next(asyncAction);
58    });
59  } else {
60    next(action);
61  }
62};
63
64// Async Action Creator
65const AsyncTodoActions = {
66  loadTodos: () => ({
67    type: 'ASYNC_ACTION',
68    payload: (dispatch: (action: Action) => void) => {
69      // API call simulation
70      setTimeout(() => {
71        dispatch({
72          type: 'LOAD_TODOS_SUCCESS',
73          payload: {
74            todos: [
75              { id: '1', text: 'Todo from API', completed: false, createdAt: new Date() }
76            ]
77          }
78        });
79      }, 1000);
80    }
81  })
82};
83
84// Using Enhanced Dispatcher
85const enhancedDispatcher = new EnhancedDispatcher();
86enhancedDispatcher.use(loggingMiddleware);
87enhancedDispatcher.use(debugMiddleware);
88enhancedDispatcher.use(asyncMiddleware);
89
90const enhancedTodoStore = new TodoStore(enhancedDispatcher);

Event Sourcing - Event history as source of truth

Event Sourcing is a pattern where application state is determined by a sequence of events, instead of storing the current state directly.

Basic Event Sourcing implementation

1// Types for Event Sourcing
2interface Event {
3  id: string;
4  type: string;
5  data: any;
6  timestamp: Date;
7  version: number;
8}
9
10interface Aggregate {
11  id: string;
12  version: number;
13  apply(event: Event): void;
14}
15
16// Event Store - stores all events
17class EventStore {
18  private events: Map<string, Event[]> = new Map();
19
20  async saveEvents(aggregateId: string, events: Event[], expectedVersion: number): Promise<void> {
21    const existingEvents = this.events.get(aggregateId) || [];
22
23    if (existingEvents.length !== expectedVersion) {
24      throw new Error('Concurrency conflict: unexpected version');
25    }
26
27    // Adding new events
28    const newEvents = events.map((event, index) => ({
29      ...event,
30      version: expectedVersion + index + 1,
31      timestamp: new Date()
32    }));
33
34    this.events.set(aggregateId, [...existingEvents, ...newEvents]);
35    console.log(\`Saved \${newEvents.length} events for aggregate \${aggregateId}\`);
36  }
37
38  async getEvents(aggregateId: string, fromVersion: number = 0): Promise<Event[]> {
39    const events = this.events.get(aggregateId) || [];
40    return events.filter(event => event.version > fromVersion);
41  }
42
43  async getAllEvents(): Promise<Event[]> {
44    const allEvents: Event[] = [];
45    for (const events of this.events.values()) {
46      allEvents.push(...events);
47    }
48    return allEvents.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
49  }
50}
51
52// Event definitions
53const UserEvents = {
54  UserCreated: (data: { name: string; email: string }) => ({
55    type: 'UserCreated',
56    data
57  }),
58
59  UserEmailChanged: (data: { email: string }) => ({
60    type: 'UserEmailChanged',
61    data
62  }),
63
64  UserDeactivated: (data: { reason: string }) => ({
65    type: 'UserDeactivated',
66    data
67  }),
68
69  UserReactivated: () => ({
70    type: 'UserReactivated',
71    data: {}
72  })
73};
74
75// User Aggregate
76class User implements Aggregate {
77  id: string;
78  version: number = 0;
79  name: string = '';
80  email: string = '';
81  isActive: boolean = true;
82  createdAt?: Date;
83  deactivatedAt?: Date;
84
85  constructor(id: string) {
86    this.id = id;
87  }
88
89  // Methods for creating events (commands)
90  static create(id: string, name: string, email: string): { user: User; events: any[] } {
91    const user = new User(id);
92    const events = [UserEvents.UserCreated({ name, email })];
93    return { user, events };
94  }
95
96  changeEmail(newEmail: string): any[] {
97    if (this.email === newEmail) {
98      return []; // No changes
99    }
100
101    if (!this.isActive) {
102      throw new Error('Cannot change email for deactivated user');
103    }
104
105    return [UserEvents.UserEmailChanged({ email: newEmail })];
106  }
107
108  deactivate(reason: string): any[] {
109    if (!this.isActive) {
110      return []; // Already inactive
111    }
112
113    return [UserEvents.UserDeactivated({ reason })];
114  }
115
116  reactivate(): any[] {
117    if (this.isActive) {
118      return []; // Already active
119    }
120
121    return [UserEvents.UserReactivated()];
122  }
123
124  // Applying events to change state
125  apply(event: Event): void {
126    switch (event.type) {
127      case 'UserCreated':
128        this.name = event.data.name;
129        this.email = event.data.email;
130        this.createdAt = event.timestamp;
131        this.isActive = true;
132        break;
133
134      case 'UserEmailChanged':
135        this.email = event.data.email;
136        break;
137
138      case 'UserDeactivated':
139        this.isActive = false;
140        this.deactivatedAt = event.timestamp;
141        break;
142
143      case 'UserReactivated':
144        this.isActive = true;
145        this.deactivatedAt = undefined;
146        break;
147
148      default:
149        throw new Error(\`Unknown event type: \${event.type}\`);
150    }
151
152    this.version = event.version;
153  }
154
155  // Reconstructing state from events
156  static fromHistory(id: string, events: Event[]): User {
157    const user = new User(id);
158    events.forEach(event => user.apply(event));
159    return user;
160  }
161}
162
163// Repository for User Aggregate
164class UserRepository {
165  constructor(private eventStore: EventStore) {}
166
167  async save(user: User, newEvents: any[]): Promise<void> {
168    if (newEvents.length === 0) return;
169
170    const events = newEvents.map(eventData => ({
171      id: Math.random().toString(36).substr(2, 9),
172      type: eventData.type,
173      data: eventData.data,
174      timestamp: new Date(),
175      version: 0 // Will be set in EventStore
176    }));
177
178    await this.eventStore.saveEvents(user.id, events, user.version);
179
180    // Applying events to aggregate
181    events.forEach(event => user.apply(event));
182  }
183
184  async getById(id: string): Promise<User | null> {
185    const events = await this.eventStore.getEvents(id);
186
187    if (events.length === 0) {
188      return null;
189    }
190
191    return User.fromHistory(id, events);
192  }
193}
194
195// Using Event Sourcing
196async function demonstrateEventSourcing() {
197  const eventStore = new EventStore();
198  const userRepository = new UserRepository(eventStore);
199
200  // Creating a user
201  const { user, events } = User.create('user1', 'Jan Kowalski', 'jan@example.com');
202  await userRepository.save(user, events);
203
204  console.log('User after creation:', user);
205
206  // Changing email
207  const emailChangeEvents = user.changeEmail('jan.kowalski@example.com');
208  await userRepository.save(user, emailChangeEvents);
209
210  console.log('User after email change:', user);
211
212  // Deactivation
213  const deactivateEvents = user.deactivate('User requested account deletion');
214  await userRepository.save(user, deactivateEvents);
215
216  console.log('User after deactivation:', user);
217
218  // Reactivation
219  const reactivateEvents = user.reactivate();
220  await userRepository.save(user, reactivateEvents);
221
222  console.log('User after reactivation:', user);
223
224  // Reconstructing state from events
225  const reconstructedUser = await userRepository.getById('user1');
226  console.log('Reconstructed user:', reconstructedUser);
227
228  // History of all events
229  const allEvents = await eventStore.getAllEvents();
230  console.log('Event history:', allEvents);
231}
232
233demonstrateEventSourcing();

Projectors and Read Models

1// Projector - creates read models from events
2abstract class Projector {
3  abstract getProjectionName(): string;
4  abstract handle(event: Event): Promise<void>;
5}
6
7// Read Model for user statistics
8interface UserStats {
9  totalUsers: number;
10  activeUsers: number;
11  deactivatedUsers: number;
12  emailChanges: number;
13}
14
15class UserStatsProjector extends Projector {
16  private stats: UserStats = {
17    totalUsers: 0,
18    activeUsers: 0,
19    deactivatedUsers: 0,
20    emailChanges: 0
21  };
22
23  getProjectionName(): string {
24    return 'UserStats';
25  }
26
27  async handle(event: Event): Promise<void> {
28    switch (event.type) {
29      case 'UserCreated':
30        this.stats.totalUsers++;
31        this.stats.activeUsers++;
32        break;
33
34      case 'UserEmailChanged':
35        this.stats.emailChanges++;
36        break;
37
38      case 'UserDeactivated':
39        this.stats.activeUsers--;
40        this.stats.deactivatedUsers++;
41        break;
42
43      case 'UserReactivated':
44        this.stats.activeUsers++;
45        this.stats.deactivatedUsers--;
46        break;
47    }
48
49    console.log(\`[UserStatsProjector] Updated stats:\`, this.stats);
50  }
51
52  getStats(): UserStats {
53    return { ...this.stats };
54  }
55}
56
57// Event Bus for projectors
58class EventBus {
59  private projectors: Projector[] = [];
60
61  registerProjector(projector: Projector): void {
62    this.projectors.push(projector);
63  }
64
65  async publishEvent(event: Event): Promise<void> {
66    console.log(\`Publishing event: \${event.type}\`);
67
68    const promises = this.projectors.map(projector =>
69      projector.handle(event).catch(error => {
70        console.error(\`Error in projector \${projector.getProjectionName()}:\`, error);
71      })
72    );
73
74    await Promise.all(promises);
75  }
76}
77
78// Enhanced Event Store with Event Bus
79class EnhancedEventStore extends EventStore {
80  constructor(private eventBus: EventBus) {
81    super();
82  }
83
84  async saveEvents(aggregateId: string, events: Event[], expectedVersion: number): Promise<void> {
85    await super.saveEvents(aggregateId, events, expectedVersion);
86
87    // Publishing events to projectors
88    const savedEvents = await this.getEvents(aggregateId, expectedVersion);
89    for (const event of savedEvents) {
90      await this.eventBus.publishEvent(event);
91    }
92  }
93}
94
95// Usage with projectors
96async function demonstrateProjectors() {
97  const eventBus = new EventBus();
98  const userStatsProjector = new UserStatsProjector();
99
100  eventBus.registerProjector(userStatsProjector);
101
102  const enhancedEventStore = new EnhancedEventStore(eventBus);
103  const userRepository = new UserRepository(enhancedEventStore);
104
105  // Performing operations - projectors will be automatically updated
106  const { user, events } = User.create('user2', 'Anna Nowak', 'anna@example.com');
107  await userRepository.save(user, events);
108
109  const emailChangeEvents = user.changeEmail('anna.nowak@example.com');
110  await userRepository.save(user, emailChangeEvents);
111
112  console.log('Final statistics:', userStatsProjector.getStats());
113}
114
115demonstrateProjectors();

State Machines with XState

State machines allow modeling complex state flows in a declarative and predictable way.

Basic state machine

1// Simple state machine implementation (without XState)
2interface StateDefinition {
3  on: Record<string, string>;
4  entry?: () => void;
5  exit?: () => void;
6}
7
8interface MachineConfig {
9  id: string;
10  initial: string;
11  states: Record<string, StateDefinition>;
12}
13
14class StateMachine {
15  private currentState: string;
16  private config: MachineConfig;
17  private listeners: Array<(state: string) => void> = [];
18
19  constructor(config: MachineConfig) {
20    this.config = config;
21    this.currentState = config.initial;
22    this.executeEntry();
23  }
24
25  getCurrentState(): string {
26    return this.currentState;
27  }
28
29  send(event: string): void {
30    const currentStateConfig = this.config.states[this.currentState];
31    const nextState = currentStateConfig.on[event];
32
33    if (!nextState) {
34      console.warn(\`Event "\${event}" not handled in state "\${this.currentState}"\`);
35      return;
36    }
37
38    this.transition(nextState);
39  }
40
41  private transition(nextState: string): void {
42    if (!this.config.states[nextState]) {
43      throw new Error(\`State "\${nextState}" not defined\`);
44    }
45
46    console.log(\`Transition: \${this.currentState} -> \${nextState}\`);
47
48    // Exit current state
49    this.executeExit();
50
51    // Change state
52    const previousState = this.currentState;
53    this.currentState = nextState;
54
55    // Entry new state
56    this.executeEntry();
57
58    // Notify listeners
59    this.listeners.forEach(listener => listener(this.currentState));
60  }
61
62  private executeEntry(): void {
63    const stateConfig = this.config.states[this.currentState];
64    if (stateConfig.entry) {
65      stateConfig.entry();
66    }
67  }
68
69  private executeExit(): void {
70    const stateConfig = this.config.states[this.currentState];
71    if (stateConfig.exit) {
72      stateConfig.exit();
73    }
74  }
75
76  subscribe(listener: (state: string) => void): () => void {
77    this.listeners.push(listener);
78    return () => {
79      const index = this.listeners.indexOf(listener);
80      if (index > -1) {
81        this.listeners.splice(index, 1);
82      }
83    };
84  }
85}
86
87// Example: State machine for order process
88const orderMachine = new StateMachine({
89  id: 'order',
90  initial: 'cart',
91  states: {
92    cart: {
93      on: {
94        CHECKOUT: 'checkout'
95      },
96      entry: () => console.log('Entered cart state'),
97      exit: () => console.log('Exited cart state')
98    },
99    checkout: {
100      on: {
101        PAYMENT: 'payment',
102        BACK_TO_CART: 'cart'
103      },
104      entry: () => console.log('Entered checkout state')
105    },
106    payment: {
107      on: {
108        SUCCESS: 'confirmed',
109        FAILURE: 'payment_failed',
110        BACK_TO_CHECKOUT: 'checkout'
111      },
112      entry: () => console.log('Processing payment...')
113    },
114    payment_failed: {
115      on: {
116        RETRY: 'payment',
117        BACK_TO_CHECKOUT: 'checkout'
118      },
119      entry: () => console.log('Payment failed!')
120    },
121    confirmed: {
122      on: {
123        FULFILL: 'shipped'
124      },
125      entry: () => console.log('Order confirmed!')
126    },
127    shipped: {
128      on: {
129        DELIVER: 'delivered'
130      },
131      entry: () => console.log('Order shipped!')
132    },
133    delivered: {
134      on: {},
135      entry: () => console.log('Order delivered!')
136    }
137  }
138});
139
140// Subscribe to state changes
141orderMachine.subscribe((state) => {
142  console.log(\`Current state: \${state}\`);
143});
144
145// Simulate order flow
146console.log('=== Order Flow Simulation ===');
147orderMachine.send('CHECKOUT');
148orderMachine.send('PAYMENT');
149orderMachine.send('SUCCESS');
150orderMachine.send('FULFILL');
151orderMachine.send('DELIVER');

XState integration (conceptual)

1// Example of what a state machine would look like with XState
2// (conceptual code - requires XState installation)
3
4/*
5import { createMachine, interpret } from 'xstate';
6
7const orderMachineXState = createMachine({
8  id: 'order',
9  initial: 'cart',
10  context: {
11    items: [],
12    total: 0,
13    customer: null
14  },
15  states: {
16    cart: {
17      on: {
18        ADD_ITEM: {
19          actions: 'addItem'
20        },
21        REMOVE_ITEM: {
22          actions: 'removeItem'
23        },
24        CHECKOUT: {
25          target: 'checkout',
26          cond: 'hasItems'
27        }
28      }
29    },
30    checkout: {
31      entry: 'calculateTotal',
32      on: {
33        PAYMENT: 'payment',
34        BACK_TO_CART: 'cart'
35      }
36    },
37    payment: {
38      invoke: {
39        id: 'paymentService',
40        src: 'processPayment',
41        onDone: {
42          target: 'confirmed',
43          actions: 'savePaymentInfo'
44        },
45        onError: {
46          target: 'payment_failed',
47          actions: 'savePaymentError'
48        }
49      },
50      on: {
51        BACK_TO_CHECKOUT: 'checkout'
52      }
53    },
54    payment_failed: {
55      on: {
56        RETRY: 'payment',
57        BACK_TO_CHECKOUT: 'checkout'
58      }
59    },
60    confirmed: {
61      entry: 'sendConfirmationEmail',
62      on: {
63        FULFILL: 'shipped'
64      }
65    },
66    shipped: {
67      entry: 'sendShippingNotification',
68      on: {
69        DELIVER: 'delivered'
70      }
71    },
72    delivered: {
73      entry: 'sendDeliveryConfirmation',
74      type: 'final'
75    }
76  }
77}, {
78  actions: {
79    addItem: (context, event) => {
80      // Add item to cart
81    },
82    removeItem: (context, event) => {
83      // Remove item from cart
84    },
85    calculateTotal: (context) => {
86      // Calculate total
87    },
88    savePaymentInfo: (context, event) => {
89      // Save payment info
90    }
91  },
92  guards: {
93    hasItems: (context) => context.items.length > 0
94  },
95  services: {
96    processPayment: (context) => {
97      // Returns Promise with payment process
98      return fetch('/api/payment', {
99        method: 'POST',
100        body: JSON.stringify(context)
101      });
102    }
103  }
104});
105*/

Advanced state machines with hierarchy

1// Hierarchical state machine for a music player
2interface PlayerContext {
3  currentTrack: string | null;
4  volume: number;
5  position: number;
6  playlist: string[];
7}
8
9class HierarchicalStateMachine extends StateMachine {
10  private context: any = {};
11
12  constructor(config: MachineConfig, initialContext: any = {}) {
13    super(config);
14    this.context = { ...initialContext };
15  }
16
17  getContext(): any {
18    return { ...this.context };
19  }
20
21  updateContext(updates: any): void {
22    this.context = { ...this.context, ...updates };
23  }
24}
25
26const musicPlayerMachine = new HierarchicalStateMachine({
27  id: 'musicPlayer',
28  initial: 'stopped',
29  states: {
30    stopped: {
31      on: {
32        PLAY: 'playing',
33        LOAD: 'loading'
34      },
35      entry: () => console.log('Player stopped')
36    },
37    loading: {
38      on: {
39        LOADED: 'playing',
40        ERROR: 'error'
41      },
42      entry: () => console.log('Loading track...')
43    },
44    playing: {
45      on: {
46        PAUSE: 'paused',
47        STOP: 'stopped',
48        NEXT: 'loading',
49        PREVIOUS: 'loading'
50      },
51      entry: () => console.log('Playing music'),
52      exit: () => console.log('Stopped playing')
53    },
54    paused: {
55      on: {
56        PLAY: 'playing',
57        STOP: 'stopped'
58      },
59      entry: () => console.log('Music paused')
60    },
61    error: {
62      on: {
63        RETRY: 'loading',
64        STOP: 'stopped'
65      },
66      entry: () => console.log('Error loading track')
67    }
68  }
69}, {
70  currentTrack: null,
71  volume: 50,
72  position: 0,
73  playlist: ['track1.mp3', 'track2.mp3']
74});
75
76// Player simulation
77console.log('=== Music Player Simulation ===');
78musicPlayerMachine.send('LOAD');
79musicPlayerMachine.send('LOADED');
80musicPlayerMachine.send('PAUSE');
81musicPlayerMachine.send('PLAY');
82musicPlayerMachine.send('NEXT');

Comparing state management patterns

When to use which pattern?

1// Example application using all three patterns
2
3// 1. Flux - for simple UI state management
4class UIStateManager {
5  // Managing UI component state, forms, modals
6}
7
8// 2. Event Sourcing - for business logic with history
9class OrderEventStore {
10  // Managing orders with full change history
11}
12
13// 3. State Machines - for complex flows
14class WorkflowStateMachine {
15  // Managing business workflows (approval workflows, etc.)
16}
17
18// Combining patterns in one application
19class ApplicationStateManager {
20  private uiState: UIStateManager;
21  private orderEvents: OrderEventStore;
22  private workflows: Map<string, WorkflowStateMachine>;
23
24  constructor() {
25    this.uiState = new UIStateManager();
26    this.orderEvents = new OrderEventStore();
27    this.workflows = new Map();
28  }
29
30  // Flux for UI
31  updateUI(action: Action): void {
32    // this.uiState.dispatch(action);
33  }
34
35  // Event Sourcing for business events
36  processBusinessEvent(event: Event): void {
37    // this.orderEvents.append(event);
38  }
39
40  // State Machines for workflows
41  advanceWorkflow(workflowId: string, event: string): void {
42    const workflow = this.workflows.get(workflowId);
43    if (workflow) {
44      workflow.send(event);
45    }
46  }
47}

Best practices

1// 1. Separation of Concerns
2interface StatePattern {
3  // UI State - Flux/Redux
4  uiState: {
5    loading: boolean;
6    errors: string[];
7    notifications: any[];
8  };
9
10  // Business State - Event Sourcing
11  businessEvents: Event[];
12
13  // Workflow State - State Machines
14  workflows: Record<string, string>; // workflowId -> currentState
15}
16
17// 2. Testing patterns
18class StatePatternTests {
19  testFluxActions(): void {
20    // Test action creators, reducers, stores
21  }
22
23  testEventSourcing(): void {
24    // Test event application, projectors
25  }
26
27  testStateMachines(): void {
28    // Test state transitions, guards, actions
29  }
30}
31
32// 3. Performance considerations
33class PerformanceOptimizations {
34  // Memoization for Flux selectors
35  memoizedSelectors = new Map();
36
37  // Snapshotting for Event Sourcing
38  snapshots = new Map();
39
40  // State Machine caching
41  machineInstances = new Map();
42}

Summary of state management patterns

Each pattern has its place in modern applications:

Flux:

  • ✅ Simple unidirectional data flow
  • ✅ Easy debugging and change tracking
  • ✅ Ideal for UI state management
  • ❌ Can be over-engineering for simple applications

Event Sourcing:

  • ✅ Complete change history
  • ✅ Ability to reconstruct state at any point in time
  • ✅ Natural auditing
  • ❌ Greater implementation complexity
  • ❌ Requires more disk space

State Machines:

  • ✅ Clear modeling of complex flows
  • ✅ Impossible invalid states
  • ✅ Easy to visualize and test
  • ❌ Learning curve for the team
  • ❌ Can be overkill for simple states

Recommendations:

  • Small applications: basic state (useState, signals)
  • Medium applications: Flux/Redux for UI state
  • Large applications: combination of patterns as needed
  • Enterprise: Event Sourcing + State Machines for critical processes

Just as ancient administrators used different management systems depending on scale and needs, modern programmers should choose state management patterns tailored to the specifics of their applications.

Go to CodeWorlds