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