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 is an application architecture pattern that introduces unidirectional data flow, making application state more predictable and easier to debug.
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'));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 is a pattern where application state is determined by a sequence of events, instead of storing the current state directly.
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();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 allow modeling complex state flows in a declarative and predictable way.
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');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*/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');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}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}Each pattern has its place in modern applications:
Flux:
Event Sourcing:
State Machines:
Recommendations:
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.