State Architecture and Design Patterns
1. Flux Architecture Patterns in React
Flux is a unidirectional data flow pattern that provides predictable state management through actions, dispatcher, stores, and views.
| Flux Component | Description | Role |
|---|---|---|
| Actions | Simple objects describing state changes | Define what happened |
| Dispatcher | Central hub for action distribution | Route actions to stores |
| Stores | Hold application state and logic | Manage domain state |
| Views (Components) | React components that render UI | Display state, dispatch actions |
| Action Creators | Helper functions to create actions | Encapsulate action creation |
Example: Classic Flux Pattern Implementation
// ActionTypes.ts - Constants for action types
export const ActionTypes = {
ADD_TODO: 'ADD_TODO',
TOGGLE_TODO: 'TOGGLE_TODO',
DELETE_TODO: 'DELETE_TODO',
SET_FILTER: 'SET_FILTER'
} as const;
// Actions.ts - Action creators
import { ActionTypes } from './ActionTypes';
export const TodoActions = {
addTodo: (text: string) => ({
type: ActionTypes.ADD_TODO,
payload: { text }
}),
toggleTodo: (id: string) => ({
type: ActionTypes.TOGGLE_TODO,
payload: { id }
}),
deleteTodo: (id: string) => ({
type: ActionTypes.DELETE_TODO,
payload: { id }
}),
setFilter: (filter: 'all' | 'active' | 'completed') => ({
type: ActionTypes.SET_FILTER,
payload: { filter }
})
};
// Dispatcher.ts - Central dispatcher
import { EventEmitter } from 'events';
class AppDispatcher extends EventEmitter {
dispatch(action: any) {
this.emit('action', action);
}
register(callback: (action: any) => void) {
this.on('action', callback);
return () => this.off('action', callback);
}
}
export const dispatcher = new AppDispatcher();
// TodoStore.ts - Store managing todo state
import { EventEmitter } from 'events';
import { dispatcher } from './Dispatcher';
import { ActionTypes } from './ActionTypes';
class TodoStore extends EventEmitter {
private todos: Array<{ id: string; text: string; completed: boolean }> = [];
private filter: 'all' | 'active' | 'completed' = 'all';
constructor() {
super();
// Register with dispatcher
dispatcher.register(this.handleAction.bind(this));
}
private handleAction(action: any) {
switch (action.type) {
case ActionTypes.ADD_TODO:
this.todos.push({
id: Date.now().toString(),
text: action.payload.text,
completed: false
});
this.emit('change');
break;
case ActionTypes.TOGGLE_TODO:
this.todos = this.todos.map(todo =>
todo.id === action.payload.id
? { ...todo, completed: !todo.completed }
: todo
);
this.emit('change');
break;
case ActionTypes.DELETE_TODO:
this.todos = this.todos.filter(todo => todo.id !== action.payload.id);
this.emit('change');
break;
case ActionTypes.SET_FILTER:
this.filter = action.payload.filter;
this.emit('change');
break;
}
}
getTodos() {
return this.todos.filter(todo => {
if (this.filter === 'active') return !todo.completed;
if (this.filter === 'completed') return todo.completed;
return true;
});
}
getFilter() {
return this.filter;
}
addChangeListener(callback: () => void) {
this.on('change', callback);
}
removeChangeListener(callback: () => void) {
this.off('change', callback);
}
}
export const todoStore = new TodoStore();
// TodoList.tsx - React component (View)
import { useState, useEffect } from 'react';
import { todoStore } from './TodoStore';
import { TodoActions } from './Actions';
import { dispatcher } from './Dispatcher';
export function TodoList() {
const [todos, setTodos] = useState(todoStore.getTodos());
const [filter, setFilter] = useState(todoStore.getFilter());
useEffect(() => {
const handleChange = () => {
setTodos(todoStore.getTodos());
setFilter(todoStore.getFilter());
};
todoStore.addChangeListener(handleChange);
return () => todoStore.removeChangeListener(handleChange);
}, []);
const handleAddTodo = (text: string) => {
dispatcher.dispatch(TodoActions.addTodo(text));
};
const handleToggleTodo = (id: string) => {
dispatcher.dispatch(TodoActions.toggleTodo(id));
};
return (
<div>
<input onKeyPress={(e) => {
if (e.key === 'Enter') {
handleAddTodo(e.currentTarget.value);
e.currentTarget.value = '';
}
}} />
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleToggleTodo(todo.id)}
/>
{todo.text}
</li>
))}
</ul>
</div>
);
}
Note: Modern Redux is based on Flux principles but simplifies the pattern by using a single
store and removing the dispatcher. Redux Toolkit further simplifies Redux implementation.
2. Model-View-Update (MVU) Patterns
MVU (also known as The Elm Architecture) separates state (Model), rendering (View), and state transitions (Update) into distinct functions.
| MVU Component | Description | Responsibility |
|---|---|---|
| Model | Application state representation | Define data structure |
| View | Pure function: Model → UI | Render state to UI |
| Update | Pure function: (Model, Msg) → Model | Handle state transitions |
| Messages | Union type of all possible events | Describe user actions |
| Commands | Side effects to execute | Async operations, API calls |
Example: MVU Pattern in React with TypeScript
// Model - State definition
type Model = {
count: number;
status: 'idle' | 'loading' | 'success' | 'error';
data: string | null;
error: string | null;
};
const initialModel: Model = {
count: 0,
status: 'idle',
data: null,
error: null
};
// Messages - All possible user actions
type Msg =
| { type: 'Increment' }
| { type: 'Decrement' }
| { type: 'Reset' }
| { type: 'FetchData' }
| { type: 'FetchSuccess'; data: string }
| { type: 'FetchError'; error: string };
// Update - Pure state transition function
function update(model: Model, msg: Msg): [Model, Cmd?] {
switch (msg.type) {
case 'Increment':
return [{ ...model, count: model.count + 1 }];
case 'Decrement':
return [{ ...model, count: model.count - 1 }];
case 'Reset':
return [{ ...model, count: 0 }];
case 'FetchData':
return [
{ ...model, status: 'loading' },
fetchDataCmd() // Return command for side effect
];
case 'FetchSuccess':
return [{
...model,
status: 'success',
data: msg.data,
error: null
}];
case 'FetchError':
return [{
...model,
status: 'error',
data: null,
error: msg.error
}];
default:
return [model];
}
}
// Commands - Side effect descriptions
type Cmd = () => Promise<Msg>;
function fetchDataCmd(): Cmd {
return async () => {
try {
const response = await fetch('/api/data');
const data = await response.text();
return { type: 'FetchSuccess', data };
} catch (error) {
return { type: 'FetchError', error: (error as Error).message };
}
};
}
// View - Pure rendering function
function view(model: Model, dispatch: (msg: Msg) => void) {
return (
<div>
<h1>Count: {model.count}</h1>
<button onClick={() => dispatch({ type: 'Increment' })}>+</button>
<button onClick={() => dispatch({ type: 'Decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'Reset' })}>Reset</button>
<hr />
<button onClick={() => dispatch({ type: 'FetchData' })}>Fetch Data</button>
{model.status === 'loading' && <p>Loading...</p>}
{model.status === 'success' && <p>Data: {model.data}</p>}
{model.status === 'error' && <p>Error: {model.error}</p>}
</div>
);
}
// Runtime - Connect Model, View, Update
function useMVU<TModel, TMsg>(
initialModel: TModel,
update: (model: TModel, msg: TMsg) => [TModel, Cmd?]
) {
const [model, setModel] = useState(initialModel);
const dispatch = useCallback((msg: TMsg) => {
setModel(currentModel => {
const [newModel, cmd] = update(currentModel, msg);
// Execute command if present
if (cmd) {
cmd().then(resultMsg => dispatch(resultMsg as TMsg));
}
return newModel;
});
}, [update]);
return [model, dispatch] as const;
}
// App component
export function Counter() {
const [model, dispatch] = useMVU(initialModel, update);
return view(model, dispatch);
}
Example: MVU with Effects System
// Enhanced MVU with proper effects handling
type Effect<Msg> = {
type: 'http' | 'timer' | 'storage';
execute: () => Promise<Msg>;
};
type UpdateResult<Model, Msg> = {
model: Model;
effects?: Effect<Msg>[];
};
// Example: Todo app with MVU + Effects
type TodoModel = {
todos: Array<{ id: string; text: string; done: boolean }>;
input: string;
};
type TodoMsg =
| { type: 'UpdateInput'; text: string }
| { type: 'AddTodo' }
| { type: 'ToggleTodo'; id: string }
| { type: 'LoadTodos' }
| { type: 'TodosLoaded'; todos: any[] }
| { type: 'SaveTodos' };
function todoUpdate(model: TodoModel, msg: TodoMsg): UpdateResult<TodoModel, TodoMsg> {
switch (msg.type) {
case 'UpdateInput':
return { model: { ...model, input: msg.text } };
case 'AddTodo':
const newTodo = {
id: Date.now().toString(),
text: model.input,
done: false
};
const newModel = {
...model,
todos: [...model.todos, newTodo],
input: ''
};
return {
model: newModel,
effects: [saveTodosEffect(newModel.todos)]
};
case 'ToggleTodo':
const updatedModel = {
...model,
todos: model.todos.map(todo =>
todo.id === msg.id ? { ...todo, done: !todo.done } : todo
)
};
return {
model: updatedModel,
effects: [saveTodosEffect(updatedModel.todos)]
};
case 'LoadTodos':
return {
model,
effects: [loadTodosEffect()]
};
case 'TodosLoaded':
return {
model: { ...model, todos: msg.todos }
};
default:
return { model };
}
}
function saveTodosEffect(todos: any[]): Effect<TodoMsg> {
return {
type: 'storage',
execute: async () => {
localStorage.setItem('todos', JSON.stringify(todos));
return { type: 'SaveTodos' } as TodoMsg;
}
};
}
function loadTodosEffect(): Effect<TodoMsg> {
return {
type: 'storage',
execute: async () => {
const stored = localStorage.getItem('todos');
const todos = stored ? JSON.parse(stored) : [];
return { type: 'TodosLoaded', todos };
}
};
}
Note: MVU pattern ensures predictable state management by making all state transitions explicit
and testable. Update functions are pure, making them easy to test and reason about.
3. Unidirectional Data Flow Implementation
Implement strict unidirectional data flow to ensure predictable state updates and easier debugging.
| Flow Step | Description | Data Direction |
|---|---|---|
| 1. User Action | User interacts with UI | UI → Action |
| 2. Dispatch Action | Action dispatched to store | Action → Store |
| 3. Update State | Store updates state immutably | Store → New State |
| 4. Notify Components | Components receive new state | State → Components |
| 5. Re-render | Components re-render with new state | Components → UI |
Example: Enforcing Unidirectional Flow with Custom Hook
// useUnidirectionalState.ts - Enforce one-way data flow
import { useState, useCallback, useRef } from 'react';
type Action<T> = {
type: string;
payload?: any;
};
type Reducer<T> = (state: T, action: Action<T>) => T;
type Middleware<T> = (
action: Action<T>,
state: T,
next: (action: Action<T>) => void
) => void;
export function useUnidirectionalState<T>(
initialState: T,
reducer: Reducer<T>,
middlewares: Middleware<T>[] = []
) {
const [state, setState] = useState(initialState);
const stateRef = useRef(state);
const listenersRef = useRef<Set<(state: T) => void>>(new Set());
// Update ref on every render
stateRef.current = state;
const dispatch = useCallback((action: Action<T>) => {
console.log('📤 Action dispatched:', action.type, action.payload);
// Apply middlewares
let index = 0;
const runMiddleware = (currentAction: Action<T>) => {
if (index < middlewares.length) {
const middleware = middlewares[index++];
middleware(currentAction, stateRef.current, runMiddleware);
} else {
// All middlewares done, update state
const currentState = stateRef.current;
const newState = reducer(currentState, currentAction);
if (newState !== currentState) {
console.log('📥 State updated:', { from: currentState, to: newState });
setState(newState);
// Notify listeners
listenersRef.current.forEach(listener => listener(newState));
}
}
};
runMiddleware(action);
}, [reducer, middlewares]);
const subscribe = useCallback((listener: (state: T) => void) => {
listenersRef.current.add(listener);
return () => listenersRef.current.delete(listener);
}, []);
// Prevent direct state mutation
const protectedState = Object.freeze({ ...state });
return {
state: protectedState,
dispatch,
subscribe
};
}
// Logging middleware
function loggingMiddleware<T>(
action: Action<T>,
state: T,
next: (action: Action<T>) => void
) {
console.group(`Action: ${action.type}`);
console.log('Payload:', action.payload);
console.log('Current State:', state);
next(action);
console.groupEnd();
}
// Validation middleware
function validationMiddleware<T>(
action: Action<T>,
state: T,
next: (action: Action<T>) => void
) {
// Example: Validate action structure
if (!action.type) {
console.error('Action must have a type');
return;
}
next(action);
}
// Usage
type CounterState = { count: number; history: number[] };
function counterReducer(state: CounterState, action: Action<CounterState>) {
switch (action.type) {
case 'INCREMENT':
return {
count: state.count + 1,
history: [...state.history, state.count + 1]
};
case 'DECREMENT':
return {
count: state.count - 1,
history: [...state.history, state.count - 1]
};
case 'RESET':
return { count: 0, history: [0] };
default:
return state;
}
}
function Counter() {
const { state, dispatch } = useUnidirectionalState(
{ count: 0, history: [0] },
counterReducer,
[loggingMiddleware, validationMiddleware]
);
// ❌ This will throw an error - state is frozen!
// state.count = 5;
// ✅ Must use dispatch for state changes
return (
<div>
<h1>Count: {state.count}</h1>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
<button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
<div>History: {state.history.join(', ')}</div>
</div>
);
}
Note: Unidirectional data flow makes state changes predictable and traceable. Use tools like
Redux DevTools to visualize the flow and debug state transitions.
4. State Composition and Module Boundaries
Structure state into composable modules with clear boundaries to improve maintainability and scalability.
| Composition Pattern | Description | Use Case |
|---|---|---|
| Domain slicing | Organize state by business domain | Users, Products, Orders modules |
| Feature slicing | Organize by feature/page | Auth, Dashboard, Settings features |
| Layer slicing | Separate by responsibility layer | UI state, Domain state, API state |
| Combiner functions | Merge multiple state slices | Root reducer composition |
| Shared state | Cross-module shared state | Theme, locale, auth tokens |
Example: Domain-Based State Composition
// Domain-based module structure
// src/domains/user/userSlice.ts
import { createSlice } from '@reduxjs/toolkit';
const userSlice = createSlice({
name: 'user',
initialState: {
profile: null,
preferences: {},
loading: false
},
reducers: {
setProfile: (state, action) => {
state.profile = action.payload;
},
updatePreferences: (state, action) => {
state.preferences = { ...state.preferences, ...action.payload };
}
}
});
export const userActions = userSlice.actions;
export const userReducer = userSlice.reducer;
// Selectors for this domain
export const userSelectors = {
selectProfile: (state: RootState) => state.user.profile,
selectPreferences: (state: RootState) => state.user.preferences,
selectIsLoading: (state: RootState) => state.user.loading
};
// src/domains/product/productSlice.ts
const productSlice = createSlice({
name: 'product',
initialState: {
items: [],
selectedId: null,
filters: {}
},
reducers: {
setProducts: (state, action) => {
state.items = action.payload;
},
selectProduct: (state, action) => {
state.selectedId = action.payload;
},
setFilters: (state, action) => {
state.filters = action.payload;
}
}
});
export const productActions = productSlice.actions;
export const productReducer = productSlice.reducer;
export const productSelectors = {
selectAllProducts: (state: RootState) => state.product.items,
selectSelectedProduct: (state: RootState) =>
state.product.items.find(p => p.id === state.product.selectedId),
selectFilters: (state: RootState) => state.product.filters
};
// src/domains/cart/cartSlice.ts
const cartSlice = createSlice({
name: 'cart',
initialState: {
items: [],
total: 0
},
reducers: {
addItem: (state, action) => {
state.items.push(action.payload);
state.total = state.items.reduce((sum, item) => sum + item.price, 0);
},
removeItem: (state, action) => {
state.items = state.items.filter(item => item.id !== action.payload);
state.total = state.items.reduce((sum, item) => sum + item.price, 0);
},
clearCart: (state) => {
state.items = [];
state.total = 0;
}
}
});
export const cartActions = cartSlice.actions;
export const cartReducer = cartSlice.reducer;
export const cartSelectors = {
selectCartItems: (state: RootState) => state.cart.items,
selectCartTotal: (state: RootState) => state.cart.total,
selectCartCount: (state: RootState) => state.cart.items.length
};
// src/store/rootReducer.ts - Compose all domain reducers
import { combineReducers } from '@reduxjs/toolkit';
import { userReducer } from '../domains/user/userSlice';
import { productReducer } from '../domains/product/productSlice';
import { cartReducer } from '../domains/cart/cartSlice';
export const rootReducer = combineReducers({
user: userReducer,
product: productReducer,
cart: cartReducer
});
export type RootState = ReturnType<typeof rootReducer>;
// src/store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import { rootReducer } from './rootReducer';
export const store = configureStore({
reducer: rootReducer
});
export type AppDispatch = typeof store.dispatch;
Example: Feature-Based State Modules with Context
// Feature-based organization using Context API
// src/features/auth/AuthContext.tsx
import { createContext, useContext, useState } from 'react';
type AuthState = {
user: User | null;
token: string | null;
isAuthenticated: boolean;
};
const AuthContext = createContext<AuthState & AuthActions | null>(null);
export function AuthProvider({ children }) {
const [state, setState] = useState<AuthState>({
user: null,
token: null,
isAuthenticated: false
});
const actions = {
login: async (credentials) => {
const { user, token } = await api.login(credentials);
setState({ user, token, isAuthenticated: true });
},
logout: () => {
setState({ user: null, token: null, isAuthenticated: false });
}
};
return (
<AuthContext.Provider value={{ ...state, ...actions }}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) throw new Error('useAuth must be within AuthProvider');
return context;
};
// src/features/dashboard/DashboardContext.tsx
const DashboardContext = createContext(null);
export function DashboardProvider({ children }) {
const [state, setState] = useState({
widgets: [],
layout: 'grid',
filters: {}
});
const actions = {
addWidget: (widget) => {
setState(s => ({ ...s, widgets: [...s.widgets, widget] }));
},
setLayout: (layout) => {
setState(s => ({ ...s, layout }));
}
};
return (
<DashboardContext.Provider value={{ ...state, ...actions }}>
{children}
</DashboardContext.Provider>
);
}
export const useDashboard = () => useContext(DashboardContext);
// src/App.tsx - Compose feature providers
export function App() {
return (
<AuthProvider>
<DashboardProvider>
<Router>
<Routes />
</Router>
</DashboardProvider>
</AuthProvider>
);
}
Note: Define clear boundaries between modules. Each module should own its state and expose only
necessary selectors and actions. Avoid tight coupling between modules.
5. State Scaling Strategies for Large Applications
Implement strategies to scale state management as applications grow in size and complexity.
| Scaling Strategy | Description | Benefit |
|---|---|---|
| Code splitting | Load state modules on demand | Reduce initial bundle size |
| Lazy reducers | Dynamically inject reducers | Load state logic when needed |
| State normalization | Flat, normalized data structures | Efficient updates, no duplication |
| Memoized selectors | Cache derived state calculations | Prevent unnecessary re-renders |
| State pagination | Load data in chunks | Handle large datasets efficiently |
| Virtual state | Windowing for large lists | Render only visible items |
Example: Dynamic Reducer Injection
// store.ts - Store with dynamic reducer injection
import { configureStore, combineReducers } from '@reduxjs/toolkit';
const staticReducers = {
auth: authReducer,
ui: uiReducer
};
export function createStore() {
const asyncReducers = {};
const store = configureStore({
reducer: createReducer(asyncReducers)
});
// Add method to inject reducers
store.injectReducer = (key, reducer) => {
if (asyncReducers[key]) return; // Already injected
asyncReducers[key] = reducer;
store.replaceReducer(createReducer(asyncReducers));
};
return store;
}
function createReducer(asyncReducers) {
return combineReducers({
...staticReducers,
...asyncReducers
});
}
export const store = createStore();
// Feature module - Lazy loaded
// src/features/analytics/index.tsx
import { lazy, useEffect } from 'react';
import { analyticsReducer } from './analyticsSlice';
import { store } from '../../store';
// Inject reducer when module loads
export function Analytics() {
useEffect(() => {
store.injectReducer('analytics', analyticsReducer);
}, []);
return <AnalyticsDashboard />;
}
// Route configuration with code splitting
const Analytics = lazy(() => import('./features/analytics'));
function App() {
return (
<Router>
<Routes>
<Route path="/analytics" element={
<Suspense fallback={<Loading />}>
<Analytics />
</Suspense>
} />
</Routes>
</Router>
);
}
Example: State Normalization for Scalability
// Normalized state structure
// ❌ BAD: Nested, denormalized data
type BadState = {
posts: Array<{
id: string;
title: string;
author: {
id: string;
name: string;
email: string;
};
comments: Array<{
id: string;
text: string;
author: {
id: string;
name: string;
};
}>;
}>;
};
// ✅ GOOD: Normalized, flat structure
type GoodState = {
posts: {
byId: Record<string, Post>;
allIds: string[];
};
users: {
byId: Record<string, User>;
allIds: string[];
};
comments: {
byId: Record<string, Comment>;
allIds: string[];
};
};
// normalizr library usage
import { normalize, schema } from 'normalizr';
// Define schemas
const userSchema = new schema.Entity('users');
const commentSchema = new schema.Entity('comments', {
author: userSchema
});
const postSchema = new schema.Entity('posts', {
author: userSchema,
comments: [commentSchema]
});
// Normalize API response
const apiResponse = {
id: '1',
title: 'Post 1',
author: { id: 'u1', name: 'Alice' },
comments: [
{ id: 'c1', text: 'Great!', author: { id: 'u2', name: 'Bob' } }
]
};
const normalized = normalize(apiResponse, postSchema);
// Result:
// {
// entities: {
// posts: { '1': { id: '1', title: 'Post 1', author: 'u1', comments: ['c1'] } },
// users: { 'u1': { id: 'u1', name: 'Alice' }, 'u2': { id: 'u2', name: 'Bob' } },
// comments: { 'c1': { id: 'c1', text: 'Great!', author: 'u2' } }
// },
// result: '1'
// }
// Redux slice with normalized data
const postsSlice = createSlice({
name: 'posts',
initialState: {
byId: {},
allIds: []
},
reducers: {
addPosts: (state, action) => {
const normalized = normalize(action.payload, [postSchema]);
// Merge normalized entities
Object.entries(normalized.entities.posts || {}).forEach(([id, post]) => {
state.byId[id] = post;
if (!state.allIds.includes(id)) {
state.allIds.push(id);
}
});
},
updatePost: (state, action) => {
const { id, changes } = action.payload;
if (state.byId[id]) {
state.byId[id] = { ...state.byId[id], ...changes };
}
}
}
});
// Memoized selectors for derived data
import { createSelector } from '@reduxjs/toolkit';
const selectPostsById = (state) => state.posts.byId;
const selectUsersById = (state) => state.users.byId;
const selectCommentsById = (state) => state.comments.byId;
// Denormalize for display (cached with createSelector)
export const selectPostWithDetails = createSelector(
[selectPostsById, selectUsersById, selectCommentsById, (_, postId) => postId],
(posts, users, comments, postId) => {
const post = posts[postId];
if (!post) return null;
return {
...post,
author: users[post.author],
comments: post.comments.map(commentId => ({
...comments[commentId],
author: users[comments[commentId].author]
}))
};
}
);
Example: Virtual Scrolling for Large State
// Virtual scrolling with react-window
import { FixedSizeList } from 'react-window';
import { useSelector } from 'react-redux';
function LargeList() {
// Only store IDs in state, not full objects
const itemIds = useSelector(state => state.items.allIds); // 10,000+ items
const itemsById = useSelector(state => state.items.byId);
// Render only visible rows
const Row = ({ index, style }) => {
const itemId = itemIds[index];
const item = itemsById[itemId];
return (
<div style={style}>
{item.name}
</div>
);
};
return (
<FixedSizeList
height={600}
itemCount={itemIds.length}
itemSize={50}
width="100%"
>
{Row}
</FixedSizeList>
);
}
Note: For large applications, combine multiple scaling strategies: normalize data, use memoized
selectors, implement code splitting, and virtualize large lists for optimal performance.
6. Micro-frontend State Management Strategies
Manage state across independent micro-frontends while maintaining isolation and enabling communication.
| Strategy | Description | Use Case |
|---|---|---|
| Isolated state | Each micro-app owns its state | Independent features |
| Shared state bus | Event bus for cross-app communication | Loosely coupled apps |
| Global state shell | Host app manages shared state | Auth, theme, navigation |
| Module federation | Share state libraries via webpack | Consistent state management |
| Custom events | Browser custom events for messaging | Framework-agnostic communication |
Example: Shared State Bus for Micro-frontends
// stateBus.ts - Central event bus for micro-frontends
class StateBus {
private listeners = new Map<string, Set<Function>>();
// Subscribe to state changes
subscribe(event: string, callback: Function) {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(callback);
return () => {
this.listeners.get(event)?.delete(callback);
};
}
// Publish state changes
publish(event: string, data: any) {
console.log(`📢 Event published: ${event}`, data);
this.listeners.get(event)?.forEach(callback => callback(data));
}
// Get current value (optional persistence)
private storage = new Map<string, any>();
getState(key: string) {
return this.storage.get(key);
}
setState(key: string, value: any) {
this.storage.set(key, value);
this.publish(key, value);
}
}
// Global singleton
export const stateBus = new StateBus();
// Micro-frontend A: Auth app
// apps/auth/src/AuthApp.tsx
import { stateBus } from '@shared/state-bus';
export function AuthApp() {
const [user, setUser] = useState(null);
const handleLogin = async (credentials) => {
const userData = await login(credentials);
setUser(userData);
// Publish auth state to other micro-apps
stateBus.setState('auth.user', userData);
stateBus.publish('auth.login', { user: userData });
};
const handleLogout = () => {
setUser(null);
stateBus.setState('auth.user', null);
stateBus.publish('auth.logout', {});
};
return <LoginForm onLogin={handleLogin} />;
}
// Micro-frontend B: Dashboard app
// apps/dashboard/src/DashboardApp.tsx
import { stateBus } from '@shared/state-bus';
export function DashboardApp() {
const [user, setUser] = useState(stateBus.getState('auth.user'));
useEffect(() => {
// Subscribe to auth events from other micro-apps
const unsubscribeLogin = stateBus.subscribe('auth.login', (data) => {
console.log('Dashboard: User logged in', data.user);
setUser(data.user);
});
const unsubscribeLogout = stateBus.subscribe('auth.logout', () => {
console.log('Dashboard: User logged out');
setUser(null);
});
return () => {
unsubscribeLogin();
unsubscribeLogout();
};
}, []);
if (!user) return <div>Please log in</div>;
return <div>Welcome, {user.name}</div>;
}
// Micro-frontend C: Shopping cart app
// apps/cart/src/CartApp.tsx
export function CartApp() {
const [user, setUser] = useState(null);
const [items, setItems] = useState([]);
useEffect(() => {
// Listen for auth changes
const unsubscribe = stateBus.subscribe('auth.user', (userData) => {
setUser(userData);
if (userData) {
// Load cart for logged-in user
loadCart(userData.id).then(setItems);
} else {
// Clear cart on logout
setItems([]);
}
});
return unsubscribe;
}, []);
const handleAddItem = (item) => {
setItems(prev => [...prev, item]);
// Publish cart update
stateBus.publish('cart.updated', { items: [...items, item] });
};
return <CartList items={items} onAddItem={handleAddItem} />;
}
Example: Custom Events for Framework-Agnostic Communication
// microFrontendEvents.ts - Browser custom events
export const MFEvents = {
// Dispatch custom event
dispatch: (eventName: string, detail: any) => {
const event = new CustomEvent(`mf:${eventName}`, {
detail,
bubbles: true,
composed: true
});
window.dispatchEvent(event);
},
// Listen to custom event
listen: (eventName: string, handler: (detail: any) => void) => {
const listener = (event: CustomEvent) => handler(event.detail);
window.addEventListener(`mf:${eventName}`, listener as EventListener);
return () => {
window.removeEventListener(`mf:${eventName}`, listener as EventListener);
};
}
};
// React micro-frontend
function ReactMicroApp() {
const [sharedData, setSharedData] = useState(null);
useEffect(() => {
const unlisten = MFEvents.listen('data-updated', (detail) => {
console.log('React app received:', detail);
setSharedData(detail);
});
return unlisten;
}, []);
const handleUpdate = () => {
MFEvents.dispatch('data-updated', { from: 'react', data: 'Hello' });
};
return <button onClick={handleUpdate}>Update</button>;
}
// Vue micro-frontend (different framework)
export default {
data() {
return { sharedData: null };
},
mounted() {
this.unlisten = MFEvents.listen('data-updated', (detail) => {
console.log('Vue app received:', detail);
this.sharedData = detail;
});
},
beforeUnmount() {
this.unlisten();
},
methods: {
handleUpdate() {
MFEvents.dispatch('data-updated', { from: 'vue', data: 'Bonjour' });
}
}
};
Example: Module Federation for Shared State
// webpack.config.js - Host app
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
auth: 'auth@http://localhost:3001/remoteEntry.js',
dashboard: 'dashboard@http://localhost:3002/remoteEntry.js'
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
'@reduxjs/toolkit': { singleton: true },
'react-redux': { singleton: true }
}
})
]
};
// Host app store - shared across micro-apps
// host/src/store/index.ts
import { configureStore } from '@reduxjs/toolkit';
export const store = configureStore({
reducer: {
shared: sharedReducer, // Shared global state
// Micro-apps will inject their reducers dynamically
}
});
// Expose store to micro-apps
window.__SHARED_STORE__ = store;
// Remote micro-app
// auth/src/bootstrap.tsx
import { Provider } from 'react-redux';
function AuthMicroApp() {
// Use shared store from host
const store = window.__SHARED_STORE__;
// Inject auth reducer
useEffect(() => {
store.injectReducer('auth', authReducer);
}, []);
return (
<Provider store={store}>
<AuthApp />
</Provider>
);
}
Warning: Micro-frontend state sharing should be minimal. Prefer isolated state with
event-based communication to maintain independence and avoid tight coupling between apps.
Section 20 Key Takeaways
- Flux architecture - Unidirectional flow with actions, dispatcher, stores, and views for predictable state management
- MVU pattern - Separate Model, View, and Update functions; pure update functions for testable state transitions
- Unidirectional flow - Enforce one-way data flow with middleware, immutable state updates, and action dispatching
- State composition - Organize by domain/feature, use combineReducers, maintain clear module boundaries
- Scaling strategies - Normalize data, use code splitting, lazy reducers, memoized selectors, and virtual scrolling
- Micro-frontends - Isolate state per app, use event bus for communication, share libraries via module federation