The Hidden Complexity of State Management

State management is more complex than you think. Learn how to build maintainable state architecture.

State management seems straightforward until your application grows. Here's why common approaches fail, and how to build maintainable state architecture.

Common Anti-Patterns

1. Global State

// ❌ BAD: Global state store
const globalStore = {
  user: null,
  cart: [],
  settings: {},
  notifications: []
};

// Used everywhere, modified anywhere
function updateCart(item: CartItem) {
  globalStore.cart.push(item);
  // Who's listening? What else changes?
}

2. Prop Drilling

// ❌ BAD: Passing state through multiple levels
function App() {
  const [user, setUser] = useState<User>();
  
  return (
    <Layout user={user}>
      <Sidebar user={user}>
        <Navigation user={user}>
          <UserMenu user={user} onLogout={() => setUser(null)} />
        </Navigation>
      </Sidebar>
    </Layout>
  );
}

3. Inconsistent Updates

// ❌ BAD: Multiple update paths
class CartService {
  updateQuantity(productId: string, quantity: number) {
    const item = this.cart.find(i => i.id === productId);
    item.quantity = quantity;
    this.calculateTotal();  // Sometimes forgotten
    this.updateStorage();   // Sometimes forgotten
    // What about the UI?
  }
}

Better Approaches

1. State Machines

// ✅ BETTER: Explicit state transitions
interface OrderState {
  status: 'draft' | 'pending' | 'confirmed' | 'shipped';
  data: Order;
  error?: Error;
}

class OrderStateMachine {
  private state: OrderState = {
    status: 'draft',
    data: null
  };

  private transitions = {
    draft: {
      submit: this.handleSubmit,
      cancel: this.handleCancel
    },
    pending: {
      confirm: this.handleConfirm,
      reject: this.handleReject
    },
    confirmed: {
      ship: this.handleShip
    }
  };

  dispatch(action: string, payload?: any) {
    const availableTransitions = 
      this.transitions[this.state.status];
    
    if (!availableTransitions?.[action]) {
      throw new Error(
        `Invalid action ${action} for status ${this.state.status}`
      );
    }

    const nextState = availableTransitions[action].call(
      this,
      this.state,
      payload
    );

    this.setState(nextState);
  }

  private setState(nextState: OrderState) {
    this.state = nextState;
    this.notify(this.state);
  }
}

2. Observable State

// ✅ BETTER: Reactive state management
class StateObservable<T> {
  private subscribers = new Set<(state: T) => void>();
  private currentState: T;

  constructor(initialState: T) {
    this.currentState = initialState;
  }

  subscribe(callback: (state: T) => void): () => void {
    this.subscribers.add(callback);
    callback(this.currentState);

    return () => {
      this.subscribers.delete(callback);
    };
  }

  protected emit(newState: T) {
    this.currentState = newState;
    this.subscribers.forEach(cb => cb(newState));
  }
}

class CartStore extends StateObservable<Cart> {
  addItem(item: CartItem) {
    const updated = {
      ...this.currentState,
      items: [...this.currentState.items, item],
      total: this.calculateTotal([
        ...this.currentState.items,
        item
      ])
    };

    this.emit(updated);
  }
}

3. Command Pattern

// ✅ BETTER: Explicit state modifications
interface Command {
  execute(): Promise<void>;
  undo(): Promise<void>;
}

class AddToCartCommand implements Command {
  constructor(
    private cart: CartStore,
    private item: CartItem
  ) {}

  async execute() {
    this.previousState = this.cart.getState();
    await this.cart.addItem(this.item);
  }

  async undo() {
    if (this.previousState) {
      await this.cart.setState(this.previousState);
    }
  }
}

class CommandProcessor {
  private history: Command[] = [];

  async execute(command: Command) {
    await command.execute();
    this.history.push(command);
  }

  async undo() {
    const command = this.history.pop();
    if (command) {
      await command.undo();
    }
  }
}

Advanced Patterns

1. State Selectors

// ✅ BETTER: Computed state
class UserStore extends StateObservable<UserState> {
  select<R>(selector: (state: UserState) => R): Observable<R> {
    return new Observable<R>(subscriber => {
      return this.subscribe(state => {
        const computed = selector(state);
        subscriber.next(computed);
      });
    });
  }

  // Usage
  readonly activeUsers$ = this.select(
    state => state.users.filter(u => u.isActive)
  );

  readonly userCount$ = this.select(
    state => state.users.length
  );
}

2. State Middleware

// ✅ BETTER: State change interceptors
interface Middleware<T> {
  beforeUpdate?(
    currentState: T,
    nextState: T
  ): Promise<T>;
  
  afterUpdate?(
    previousState: T,
    currentState: T
  ): Promise<void>;
}

class LoggingMiddleware<T> implements Middleware<T> {
  async beforeUpdate(
    currentState: T,
    nextState: T
  ): Promise<T> {
    console.log('State changing:', {
      from: currentState,
      to: nextState
    });
    return nextState;
  }

  async afterUpdate(
    previousState: T,
    currentState: T
  ): Promise<void> {
    console.log('State changed', {
      from: previousState,
      to: currentState
    });
  }
}

class Store<T> {
  private middlewares: Middleware<T>[] = [];

  async setState(nextState: T) {
    let finalState = nextState;

    // Run before middlewares
    for (const middleware of this.middlewares) {
      if (middleware.beforeUpdate) {
        finalState = await middleware.beforeUpdate(
          this.state,
          finalState
        );
      }
    }

    const previousState = this.state;
    this.state = finalState;

    // Run after middlewares
    for (const middleware of this.middlewares) {
      if (middleware.afterUpdate) {
        await middleware.afterUpdate(
          previousState,
          this.state
        );
      }
    }
  }
}

3. State Persistence

// ✅ BETTER: Automatic state persistence
class PersistentStore<T> extends Store<T> {
  constructor(
    private key: string,
    private storage: Storage = localStorage
  ) {
    super(this.loadInitialState());
  }

  private loadInitialState(): T {
    const stored = this.storage.getItem(this.key);
    return stored ? JSON.parse(stored) : this.getDefaultState();
  }

  protected async setState(nextState: T) {
    await super.setState(nextState);
    
    // Persist to storage
    this.storage.setItem(
      this.key,
      JSON.stringify(this.state)
    );
  }

  async clear() {
    await this.setState(this.getDefaultState());
    this.storage.removeItem(this.key);
  }
}

Best Practices

1. State Immutability

// ✅ BETTER: Immutable state updates
class TodoStore extends Store<TodoState> {
  addTodo(todo: Todo) {
    this.setState({
      ...this.state,
      todos: [...this.state.todos, todo]
    });
  }

  updateTodo(id: string, updates: Partial<Todo>) {
    this.setState({
      ...this.state,
      todos: this.state.todos.map(todo =>
        todo.id === id
          ? { ...todo, ...updates }
          : todo
      )
    });
  }
}

Conclusion

For better state management:

  • Use state machines for complex flows
  • Implement observable patterns
  • Keep state updates explicit
  • Use immutable patterns
  • Consider persistence needs

Remember: Good state management is about making state changes predictable and traceable.