State Management Without Libraries: Vanilla JavaScript Approaches

May 2, 2025

Introduction

While state management libraries like Redux, MobX, and Zustand are popular, many applications can benefit from lightweight, custom state management solutions. Building your own state management system gives you complete control, eliminates dependencies, and often results in simpler, more maintainable code.

This guide explores practical approaches to managing application state using only vanilla JavaScript, from simple reactive patterns to complex nested state architectures.

Simple Reactive State

Basic Observable State

Let's start with a simple reactive state implementation:

// Simple reactive state manager
class ReactiveState {
  constructor(initialState = {}) {
    this._state = { ...initialState };
    this._subscribers = new Set();
  }

  // Get current state (immutable)
  getState() {
    return { ...this._state };
  }

  // Subscribe to state changes
  subscribe(callback) {
    this._subscribers.add(callback);
    
    // Return unsubscribe function
    return () => {
      this._subscribers.delete(callback);
    };
  }

  // Update state and notify subscribers
  setState(updates) {
    const prevState = this.getState();
    this._state = { ...this._state, ...updates };
    
    // Notify all subscribers
    this._subscribers.forEach(callback => {
      try {
        callback(this.getState(), prevState);
      } catch (error) {
        console.error('Error in state subscriber:', error);
      }
    });
  }

  // Reset to initial state
  reset(initialState = {}) {
    this._state = { ...initialState };
    this._subscribers.forEach(callback => {
      callback(this.getState(), {});
    });
  }
}

// Usage example
const appState = new ReactiveState({
  user: null,
  theme: 'light',
  notifications: []
});

// Subscribe to state changes
const unsubscribe = appState.subscribe((newState, prevState) => {
  console.log('State changed:', {
    from: prevState,
    to: newState
  });
  
  // Update UI based on state changes
  updateUI(newState);
});

// Update state
appState.setState({
  user: { name: 'John Doe', email: 'john@example.com' },
  theme: 'dark'
});

Computed Properties and Derived State

// Enhanced state manager with computed properties
class ComputedState extends ReactiveState {
  constructor(initialState = {}, computedProperties = {}) {
    super(initialState);
    this._computed = computedProperties;
    this._computedCache = new Map();
  }

  getState() {
    const baseState = super.getState();
    const computedState = {};

    // Calculate computed properties
    Object.entries(this._computed).forEach(([key, computeFn]) => {
      try {
        // Simple memoization
        const cacheKey = JSON.stringify(baseState);
        if (this._computedCache.has(`${key}:${cacheKey}`)) {
          computedState[key] = this._computedCache.get(`${key}:${cacheKey}`);
        } else {
          const value = computeFn(baseState);
          this._computedCache.set(`${key}:${cacheKey}`, value);
          computedState[key] = value;
        }
      } catch (error) {
        console.error(`Error computing ${key}:`, error);
        computedState[key] = null;
      }
    });

    return { ...baseState, ...computedState };
  }

  setState(updates) {
    // Clear computed cache when state changes
    this._computedCache.clear();
    super.setState(updates);
  }
}

// Example with computed properties
const shoppingCartState = new ComputedState(
  {
    items: [],
    tax: 0.08,
    discountCode: null
  },
  {
    // Computed: total items count
    itemCount: (state) => state.items.length,
    
    // Computed: subtotal
    subtotal: (state) => state.items.reduce((sum, item) => {
      return sum + (item.price * item.quantity);
    }, 0),
    
    // Computed: tax amount
    taxAmount: (state) => {
      const subtotal = state.items.reduce((sum, item) => {
        return sum + (item.price * item.quantity);
      }, 0);
      return subtotal * state.tax;
    },
    
    // Computed: total with tax
    total: (state) => {
      const subtotal = state.items.reduce((sum, item) => {
        return sum + (item.price * item.quantity);
      }, 0);
      const tax = subtotal * state.tax;
      return subtotal + tax;
    },
    
    // Computed: is cart empty
    isEmpty: (state) => state.items.length === 0
  }
);

// Usage
shoppingCartState.setState({
  items: [
    { id: 1, name: 'Laptop', price: 999, quantity: 1 },
    { id: 2, name: 'Mouse', price: 29, quantity: 2 }
  ]
});

const state = shoppingCartState.getState();
console.log({
  itemCount: state.itemCount,     // 2
  subtotal: state.subtotal,       // 1057
  taxAmount: state.taxAmount,     // 84.56
  total: state.total,             // 1141.56
  isEmpty: state.isEmpty          // false
});

Action-Based State Management

Redux-Like Pattern Without Redux

// Action-based state manager
class ActionState {
  constructor(initialState = {}, reducers = {}) {
    this._state = { ...initialState };
    this._reducers = reducers;
    this._subscribers = new Set();
    this._middlewares = [];
  }

  // Add middleware for side effects
  use(middleware) {
    this._middlewares.push(middleware);
  }

  // Subscribe to state changes
  subscribe(callback) {
    this._subscribers.add(callback);
    return () => this._subscribers.delete(callback);
  }

  // Get current state
  getState() {
    return { ...this._state };
  }

  // Dispatch action
  async dispatch(action) {
    // Run middlewares
    let processedAction = action;
    
    for (const middleware of this._middlewares) {
      processedAction = await middleware(processedAction, this.getState(), this);
      if (!processedAction) break; // Middleware can cancel action
    }

    if (!processedAction) return;

    // Find and execute reducer
    const { type, payload } = processedAction;
    const reducer = this._reducers[type];

    if (!reducer) {
      console.warn(`No reducer found for action type: ${type}`);
      return;
    }

    try {
      const prevState = this.getState();
      const newState = reducer(prevState, payload);
      
      // Ensure immutability
      this._state = { ...newState };
      
      // Notify subscribers
      this._subscribers.forEach(callback => {
        callback(this.getState(), prevState, action);
      });
      
      return processedAction;
    } catch (error) {
      console.error(`Error in reducer for ${type}:`, error);
    }
  }
}

// Todo app example
const todoReducers = {
  ADD_TODO: (state, todo) => ({
    ...state,
    todos: [
      ...state.todos,
      {
        id: Date.now(),
        text: todo.text,
        completed: false,
        createdAt: new Date().toISOString()
      }
    ]
  }),

  TOGGLE_TODO: (state, { id }) => ({
    ...state,
    todos: state.todos.map(todo =>
      todo.id === id
        ? { ...todo, completed: !todo.completed }
        : todo
    )
  }),

  DELETE_TODO: (state, { id }) => ({
    ...state,
    todos: state.todos.filter(todo => todo.id !== id)
  }),

  SET_FILTER: (state, { filter }) => ({
    ...state,
    filter
  }),

  SET_LOADING: (state, { loading }) => ({
    ...state,
    loading
  })
};

// Create store
const todoStore = new ActionState(
  {
    todos: [],
    filter: 'all', // all, active, completed
    loading: false
  },
  todoReducers
);

// Middleware for logging
const loggerMiddleware = (action, state, store) => {
  console.group(`Action: ${action.type}`);
  console.log('Payload:', action.payload);
  console.log('Current State:', state);
  console.groupEnd();
  
  return action; // Continue with action
};

// Middleware for persistence
const persistenceMiddleware = (action, state, store) => {
  // Skip persistence for certain actions
  if (action.type === 'SET_LOADING') {
    return action;
  }

  // Save to localStorage after action completes
  setTimeout(() => {
    localStorage.setItem('todoState', JSON.stringify(store.getState()));
  }, 0);
  
  return action;
};

// Add middlewares
todoStore.use(loggerMiddleware);
todoStore.use(persistenceMiddleware);

// Subscribe to changes
todoStore.subscribe((newState, prevState, action) => {
  renderTodoApp(newState);
});

// Dispatch actions
todoStore.dispatch({
  type: 'ADD_TODO',
  payload: { text: 'Learn vanilla state management' }
});

todoStore.dispatch({
  type: 'ADD_TODO',
  payload: { text: 'Build awesome apps' }
});

todoStore.dispatch({
  type: 'TOGGLE_TODO',
  payload: { id: todoStore.getState().todos[0].id }
});

Async State Management

Handling Async Operations

// Async state manager with request handling
class AsyncState extends ActionState {
  constructor(initialState = {}, reducers = {}) {
    super(initialState, reducers);
    this._pendingRequests = new Map();
  }

  // Async action creator
  createAsyncAction(type, asyncFn) {
    return async (payload) => {
      const requestId = Date.now() + Math.random();
      
      // Dispatch loading state
      await this.dispatch({
        type: `${type}_LOADING`,
        payload: { requestId, loading: true }
      });

      try {
        // Store pending request for cancellation
        const abortController = new AbortController();
        this._pendingRequests.set(requestId, abortController);

        // Execute async function
        const result = await asyncFn(payload, {
          signal: abortController.signal,
          dispatch: this.dispatch.bind(this),
          getState: this.getState.bind(this)
        });

        // Dispatch success
        await this.dispatch({
          type: `${type}_SUCCESS`,
          payload: { requestId, data: result }
        });

        return result;
      } catch (error) {
        if (error.name === 'AbortError') {
          await this.dispatch({
            type: `${type}_CANCELLED`,
            payload: { requestId }
          });
        } else {
          await this.dispatch({
            type: `${type}_ERROR`,
            payload: { requestId, error: error.message }
          });
        }
        
        throw error;
      } finally {
        this._pendingRequests.delete(requestId);
      }
    };
  }

  // Cancel specific request
  cancelRequest(requestId) {
    const controller = this._pendingRequests.get(requestId);
    if (controller) {
      controller.abort();
      this._pendingRequests.delete(requestId);
    }
  }

  // Cancel all pending requests
  cancelAllRequests() {
    this._pendingRequests.forEach(controller => controller.abort());
    this._pendingRequests.clear();
  }
}

// API state management example
const apiReducers = {
  FETCH_USERS_LOADING: (state, { requestId, loading }) => ({
    ...state,
    users: {
      ...state.users,
      loading,
      error: null
    }
  }),

  FETCH_USERS_SUCCESS: (state, { data }) => ({
    ...state,
    users: {
      data,
      loading: false,
      error: null,
      lastFetched: new Date().toISOString()
    }
  }),

  FETCH_USERS_ERROR: (state, { error }) => ({
    ...state,
    users: {
      ...state.users,
      loading: false,
      error
    }
  }),

  FETCH_USER_PROFILE_SUCCESS: (state, { data }) => ({
    ...state,
    currentUser: data,
    userProfile: {
      ...state.userProfile,
      loading: false,
      error: null
    }
  })
};

const apiStore = new AsyncState(
  {
    users: {
      data: [],
      loading: false,
      error: null,
      lastFetched: null
    },
    currentUser: null,
    userProfile: {
      loading: false,
      error: null
    }
  },
  apiReducers
);

// Create async actions
const fetchUsers = apiStore.createAsyncAction('FETCH_USERS', async (params, { signal }) => {
  const response = await fetch('/api/users?' + new URLSearchParams(params), { signal });
  
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }
  
  return response.json();
});

const fetchUserProfile = apiStore.createAsyncAction('FETCH_USER_PROFILE', async (userId, { signal, dispatch, getState }) => {
  // Check cache first
  const currentState = getState();
  if (currentState.currentUser?.id === userId) {
    return currentState.currentUser;
  }

  const response = await fetch(`/api/users/${userId}`, { signal });
  return response.json();
});

// Usage
async function loadUserData() {
  try {
    await fetchUsers({ page: 1, limit: 10 });
    const users = apiStore.getState().users.data;
    
    if (users.length > 0) {
      await fetchUserProfile(users[0].id);
    }
  } catch (error) {
    console.error('Failed to load user data:', error);
  }
}

Nested State Management

Complex State Structures

// Deep state manager with path-based updates
class DeepState {
  constructor(initialState = {}) {
    this._state = this._deepClone(initialState);
    this._subscribers = new Set();
    this._pathSubscribers = new Map();
  }

  _deepClone(obj) {
    if (obj === null || typeof obj !== 'object') return obj;
    if (obj instanceof Date) return new Date(obj);
    if (obj instanceof Array) return obj.map(item => this._deepClone(item));
    
    const cloned = {};
    Object.keys(obj).forEach(key => {
      cloned[key] = this._deepClone(obj[key]);
    });
    return cloned;
  }

  _getByPath(obj, path) {
    return path.split('.').reduce((current, key) => {
      return current && current[key] !== undefined ? current[key] : undefined;
    }, obj);
  }

  _setByPath(obj, path, value) {
    const keys = path.split('.');
    const lastKey = keys.pop();
    
    const target = keys.reduce((current, key) => {
      if (!current[key] || typeof current[key] !== 'object') {
        current[key] = {};
      }
      return current[key];
    }, obj);
    
    target[lastKey] = value;
  }

  // Subscribe to specific path changes
  subscribeToPath(path, callback) {
    if (!this._pathSubscribers.has(path)) {
      this._pathSubscribers.set(path, new Set());
    }
    
    this._pathSubscribers.get(path).add(callback);
    
    return () => {
      const subscribers = this._pathSubscribers.get(path);
      if (subscribers) {
        subscribers.delete(callback);
        if (subscribers.size === 0) {
          this._pathSubscribers.delete(path);
        }
      }
    };
  }

  // Get value at path
  get(path = '') {
    if (!path) return this._deepClone(this._state);
    return this._deepClone(this._getByPath(this._state, path));
  }

  // Set value at path
  set(path, value) {
    const prevState = this._deepClone(this._state);
    const prevValue = this._getByPath(this._state, path);
    
    this._setByPath(this._state, path, value);
    
    // Notify path subscribers
    const pathSubscribers = this._pathSubscribers.get(path);
    if (pathSubscribers) {
      pathSubscribers.forEach(callback => {
        callback(this._deepClone(value), this._deepClone(prevValue), path);
      });
    }
    
    // Notify general subscribers
    this._subscribers.forEach(callback => {
      callback(this._deepClone(this._state), prevState);
    });
  }

  // Update multiple paths at once
  update(updates) {
    const prevState = this._deepClone(this._state);
    const changedPaths = [];
    
    Object.entries(updates).forEach(([path, value]) => {
      this._setByPath(this._state, path, value);
      changedPaths.push(path);
    });
    
    // Notify path subscribers for changed paths
    changedPaths.forEach(path => {
      const pathSubscribers = this._pathSubscribers.get(path);
      if (pathSubscribers) {
        const currentValue = this._getByPath(this._state, path);
        const prevValue = this._getByPath(prevState, path);
        
        pathSubscribers.forEach(callback => {
          callback(
            this._deepClone(currentValue), 
            this._deepClone(prevValue), 
            path
          );
        });
      }
    });
    
    // Notify general subscribers
    this._subscribers.forEach(callback => {
      callback(this._deepClone(this._state), prevState);
    });
  }

  // Subscribe to all changes
  subscribe(callback) {
    this._subscribers.add(callback);
    return () => this._subscribers.delete(callback);
  }

  // Array helpers
  push(path, item) {
    const array = this.get(path) || [];
    this.set(path, [...array, item]);
  }

  remove(path, predicate) {
    const array = this.get(path) || [];
    const filtered = array.filter(item => !predicate(item));
    this.set(path, filtered);
  }

  // Object helpers
  merge(path, updates) {
    const current = this.get(path) || {};
    this.set(path, { ...current, ...updates });
  }
}

// Complex app state example
const appStore = new DeepState({
  ui: {
    modals: {
      login: { open: false, data: null },
      confirmation: { open: false, data: null }
    },
    navigation: {
      currentPage: 'home',
      breadcrumbs: []
    },
    theme: {
      mode: 'light',
      primaryColor: '#007bff'
    }
  },
  user: {
    profile: null,
    preferences: {
      notifications: true,
      theme: 'auto'
    },
    permissions: []
  },
  data: {
    posts: [],
    comments: {},
    categories: []
  }
});

// Subscribe to specific UI changes
appStore.subscribeToPath('ui.modals.login', (newValue, prevValue) => {
  if (newValue.open && !prevValue.open) {
    console.log('Login modal opened');
    document.body.classList.add('modal-open');
  } else if (!newValue.open && prevValue.open) {
    console.log('Login modal closed');
    document.body.classList.remove('modal-open');
  }
});

// Subscribe to theme changes
appStore.subscribeToPath('ui.theme.mode', (newMode, prevMode) => {
  document.documentElement.setAttribute('data-theme', newMode);
});

// Usage examples
appStore.set('ui.modals.login.open', true);
appStore.merge('user.profile', { 
  name: 'John Doe', 
  email: 'john@example.com' 
});

appStore.push('data.posts', {
  id: 1,
  title: 'First Post',
  content: 'Hello World!'
});

// Batch updates
appStore.update({
  'ui.navigation.currentPage': 'profile',
  'ui.navigation.breadcrumbs': ['Home', 'Profile'],
  'user.preferences.notifications': false
});

Performance Optimization

Efficient State Updates

// Optimized state manager with batching and memoization
class OptimizedState {
  constructor(initialState = {}) {
    this._state = initialState;
    this._subscribers = new Set();
    this._updateQueue = [];
    this._isUpdating = false;
    this._memo = new WeakMap();
  }

  // Batch multiple updates
  batch(updateFn) {
    this._isUpdating = true;
    const prevState = { ...this._state };
    
    try {
      updateFn({
        set: (updates) => {
          this._updateQueue.push(updates);
        }
      });

      // Apply all queued updates
      if (this._updateQueue.length > 0) {
        const mergedUpdates = this._updateQueue.reduce((acc, updates) => {
          return { ...acc, ...updates };
        }, {});
        
        this._state = { ...this._state, ...mergedUpdates };
        this._updateQueue = [];
        
        // Notify subscribers once
        this._subscribers.forEach(callback => {
          callback(this._state, prevState);
        });
      }
    } finally {
      this._isUpdating = false;
    }
  }

  // Memoized selector
  createSelector(selectorFn, dependencies = []) {
    return () => {
      const cacheKey = JSON.stringify([
        selectorFn.toString(),
        ...dependencies.map(dep => this._state[dep])
      ]);
      
      if (this._memo.has(selectorFn) && this._memo.get(selectorFn).key === cacheKey) {
        return this._memo.get(selectorFn).value;
      }
      
      const result = selectorFn(this._state);
      this._memo.set(selectorFn, { key: cacheKey, value: result });
      
      return result;
    };
  }

  // Debounced state updates
  createDebouncedUpdater(delay = 300) {
    let timeoutId;
    
    return (updates) => {
      clearTimeout(timeoutId);
      
      timeoutId = setTimeout(() => {
        this.setState(updates);
      }, delay);
    };
  }

  setState(updates) {
    if (this._isUpdating) {
      this._updateQueue.push(updates);
      return;
    }

    const prevState = { ...this._state };
    this._state = { ...this._state, ...updates };
    
    // Clear memoization cache
    this._memo = new WeakMap();
    
    this._subscribers.forEach(callback => {
      callback(this._state, prevState);
    });
  }

  getState() {
    return this._state;
  }

  subscribe(callback) {
    this._subscribers.add(callback);
    return () => this._subscribers.delete(callback);
  }
}

// Usage example
const optimizedStore = new OptimizedState({
  users: [],
  posts: [],
  comments: [],
  filters: {
    searchTerm: '',
    category: 'all',
    sortBy: 'date'
  }
});

// Create memoized selectors
const selectFilteredPosts = optimizedStore.createSelector(
  (state) => {
    return state.posts.filter(post => {
      if (state.filters.category !== 'all' && post.category !== state.filters.category) {
        return false;
      }
      
      if (state.filters.searchTerm) {
        return post.title.toLowerCase().includes(state.filters.searchTerm.toLowerCase());
      }
      
      return true;
    }).sort((a, b) => {
      if (state.filters.sortBy === 'date') {
        return new Date(b.createdAt) - new Date(a.createdAt);
      }
      return a.title.localeCompare(b.title);
    });
  },
  ['posts', 'filters'] // Dependencies
);

// Create debounced search updater
const updateSearch = optimizedStore.createDebouncedUpdater(300);

// Batch multiple updates
optimizedStore.batch(({ set }) => {
  set({ 'filters.category': 'tech' });
  set({ 'filters.sortBy': 'title' });
});

// Usage in search input
document.getElementById('search').addEventListener('input', (e) => {
  updateSearch({ 'filters.searchTerm': e.target.value });
});

Local Storage Integration

Persistent State

// State manager with localStorage persistence
class PersistentState {
  constructor(key, initialState = {}, options = {}) {
    this.storageKey = key;
    this.options = {
      serialize: JSON.stringify,
      deserialize: JSON.parse,
      whitelist: null, // Array of keys to persist
      blacklist: null, // Array of keys to exclude
      version: 1,
      migrate: null,
      ...options
    };
    
    this._state = this._loadState(initialState);
    this._subscribers = new Set();
    
    // Auto-save on page unload
    window.addEventListener('beforeunload', () => {
      this._saveState();
    });
    
    // Auto-save periodically
    setInterval(() => this._saveState(), 30000); // Every 30 seconds
  }

  _loadState(initialState) {
    try {
      const serializedState = localStorage.getItem(this.storageKey);
      
      if (!serializedState) {
        return { ...initialState };
      }
      
      const parsedState = this.options.deserialize(serializedState);
      
      // Version migration
      if (parsedState._version !== this.options.version && this.options.migrate) {
        const migratedState = this.options.migrate(parsedState, parsedState._version, this.options.version);
        return { ...initialState, ...migratedState, _version: this.options.version };
      }
      
      return { ...initialState, ...parsedState };
    } catch (error) {
      console.warn('Failed to load state from localStorage:', error);
      return { ...initialState };
    }
  }

  _saveState() {
    try {
      let stateToSave = { ...this._state, _version: this.options.version };
      
      // Apply whitelist/blacklist
      if (this.options.whitelist) {
        stateToSave = this.options.whitelist.reduce((filtered, key) => {
          if (this._state[key] !== undefined) {
            filtered[key] = this._state[key];
          }
          return filtered;
        }, { _version: this.options.version });
      }
      
      if (this.options.blacklist) {
        this.options.blacklist.forEach(key => {
          delete stateToSave[key];
        });
      }
      
      const serializedState = this.options.serialize(stateToSave);
      localStorage.setItem(this.storageKey, serializedState);
    } catch (error) {
      console.warn('Failed to save state to localStorage:', error);
    }
  }

  getState() {
    return { ...this._state };
  }

  setState(updates) {
    const prevState = this.getState();
    this._state = { ...this._state, ...updates };
    
    // Save immediately for important updates
    this._saveState();
    
    this._subscribers.forEach(callback => {
      callback(this.getState(), prevState);
    });
  }

  subscribe(callback) {
    this._subscribers.add(callback);
    return () => this._subscribers.delete(callback);
  }

  // Clear persisted state
  clear() {
    localStorage.removeItem(this.storageKey);
    this._state = {};
    this._subscribers.forEach(callback => {
      callback({}, this._state);
    });
  }
}

// Usage example with migration
const userPreferences = new PersistentState('userPrefs', {
  theme: 'light',
  language: 'en',
  notifications: true,
  sidebarCollapsed: false
}, {
  whitelist: ['theme', 'language', 'notifications'], // Don't persist UI state
  version: 2,
  migrate: (state, fromVersion, toVersion) => {
    console.log(`Migrating from version ${fromVersion} to ${toVersion}`);
    
    if (fromVersion === 1 && toVersion === 2) {
      // Migration logic from v1 to v2
      return {
        ...state,
        theme: state.darkMode ? 'dark' : 'light', // Convert old darkMode boolean
        notifications: state.enableNotifications !== false // Default to true
      };
    }
    
    return state;
  }
});

Testing State Management

Unit Testing State Logic

// Test utilities for state management
class StateTestUtils {
  static createMockState(initialState = {}) {
    const state = new ReactiveState(initialState);
    const stateHistory = [state.getState()];
    
    // Track state changes
    state.subscribe((newState) => {
      stateHistory.push(newState);
    });
    
    return {
      state,
      getHistory: () => [...stateHistory],
      getLastState: () => stateHistory[stateHistory.length - 1],
      getStateChanges: () => stateHistory.length - 1,
      expectState: (expectedState) => {
        const current = state.getState();
        const matches = Object.keys(expectedState).every(key => {
          return JSON.stringify(current[key]) === JSON.stringify(expectedState[key]);
        });
        
        if (!matches) {
          throw new Error(`State mismatch:\nExpected: ${JSON.stringify(expectedState, null, 2)}\nActual: ${JSON.stringify(current, null, 2)}`);
        }
        
        return true;
      }
    };
  }

  static async testAsyncAction(actionFn, initialState = {}) {
    const mockState = this.createMockState(initialState);
    const results = [];
    
    try {
      const result = await actionFn(mockState.state);
      results.push({ type: 'success', result });
    } catch (error) {
      results.push({ type: 'error', error });
    }
    
    return {
      ...mockState,
      results,
      success: results.some(r => r.type === 'success'),
      error: results.find(r => r.type === 'error')?.error
    };
  }
}

// Example tests
function runStateTests() {
  console.group('🧪 State Management Tests');
  
  // Test 1: Basic state updates
  console.log('Test 1: Basic state updates');
  const { state, expectState } = StateTestUtils.createMockState({
    count: 0,
    user: null
  });
  
  state.setState({ count: 5 });
  expectState({ count: 5 });
  console.log('✅ Basic state updates work');
  
  // Test 2: Computed properties
  console.log('Test 2: Computed properties');
  const computedState = new ComputedState(
    { items: [] },
    { itemCount: (state) => state.items.length }
  );
  
  computedState.setState({ items: ['a', 'b', 'c'] });
  const result = computedState.getState();
  
  if (result.itemCount !== 3) {
    throw new Error('Computed property failed');
  }
  console.log('✅ Computed properties work');
  
  // Test 3: Async actions
  console.log('Test 3: Async actions');
  StateTestUtils.testAsyncAction(
    async (state) => {
      state.setState({ loading: true });
      
      // Simulate async operation
      await new Promise(resolve => setTimeout(resolve, 100));
      
      state.setState({ 
        loading: false, 
        data: 'test data' 
      });
      
      return 'success';
    },
    { loading: false, data: null }
  ).then(({ state, success, expectState }) => {
    if (!success) throw new Error('Async action failed');
    expectState({ loading: false, data: 'test data' });
    console.log('✅ Async actions work');
  });
  
  console.groupEnd();
}

// Run tests
runStateTests();

Best Practices and Patterns

State Architecture Guidelines

// State management best practices
const StateManagementBestPractices = {
  // 1. Single source of truth
  createAppStore() {
    return new ReactiveState({
      // UI state (ephemeral)
      ui: {
        loading: false,
        modals: {},
        notifications: []
      },
      
      // User data (persistent)
      user: {
        profile: null,
        preferences: {},
        permissions: []
      },
      
      // Application data (cached)
      data: {
        posts: [],
        categories: [],
        cache: {}
      }
    });
  },

  // 2. Immutable updates
  updateImmutably(state, path, updater) {
    const pathArray = path.split('.');
    const deepClone = (obj) => JSON.parse(JSON.stringify(obj));
    
    const newState = deepClone(state);
    let current = newState;
    
    // Navigate to parent
    for (let i = 0; i < pathArray.length - 1; i++) {
      current = current[pathArray[i]];
    }
    
    // Apply update
    const lastKey = pathArray[pathArray.length - 1];
    current[lastKey] = updater(current[lastKey]);
    
    return newState;
  },

  // 3. Normalized state structure
  normalizeEntities(entities, idField = 'id') {
    return {
      byId: entities.reduce((acc, entity) => {
        acc[entity[idField]] = entity;
        return acc;
      }, {}),
      allIds: entities.map(entity => entity[idField])
    };
  },

  // 4. Selector patterns
  createSelectors(state) {
    return {
      // Simple selectors
      getUser: () => state.user.profile,
      isLoading: () => state.ui.loading,
      
      // Parameterized selectors
      getPostById: (id) => state.data.posts.find(post => post.id === id),
      getPostsByCategory: (category) => 
        state.data.posts.filter(post => post.category === category),
      
      // Computed selectors
      getActiveUser: () => {
        const user = state.user.profile;
        return user?.status === 'active' ? user : null;
      },
      
      // Memoized expensive computations
      getPostsWithCommentCount: (() => {
        let cache = null;
        let lastPostsLength = 0;
        
        return () => {
          if (cache && state.data.posts.length === lastPostsLength) {
            return cache;
          }
          
          cache = state.data.posts.map(post => ({
            ...post,
            commentCount: state.data.comments[post.id]?.length || 0
          }));
          
          lastPostsLength = state.data.posts.length;
          return cache;
        };
      })()
    };
  }
};

// Usage examples
const appStore = StateManagementBestPractices.createAppStore();
const selectors = StateManagementBestPractices.createSelectors(appStore.getState());

// Normalized data example
const posts = [
  { id: 1, title: 'Post 1', content: '...' },
  { id: 2, title: 'Post 2', content: '...' }
];

const normalizedPosts = StateManagementBestPractices.normalizeEntities(posts);
appStore.setState({ 'data.posts': normalizedPosts });

Framework Integration

React-like Hooks Pattern

// React-inspired hooks for vanilla JavaScript
class VanillaHooks {
  constructor() {
    this.currentComponent = null;
    this.hookIndex = 0;
    this.hooks = new WeakMap();
  }

  withComponent(component, callback) {
    const prevComponent = this.currentComponent;
    const prevIndex = this.hookIndex;
    
    this.currentComponent = component;
    this.hookIndex = 0;
    
    if (!this.hooks.has(component)) {
      this.hooks.set(component, []);
    }
    
    try {
      return callback();
    } finally {
      this.currentComponent = prevComponent;
      this.hookIndex = prevIndex;
    }
  }

  useState(initialValue) {
    const component = this.currentComponent;
    const index = this.hookIndex++;
    const componentHooks = this.hooks.get(component);
    
    if (componentHooks[index] === undefined) {
      componentHooks[index] = {
        value: initialValue,
        setValue: (newValue) => {
          const hook = componentHooks[index];
          hook.value = typeof newValue === 'function' ? newValue(hook.value) : newValue;
          
          // Trigger re-render
          if (component.render) {
            component.render();
          }
        }
      };
    }
    
    const hook = componentHooks[index];
    return [hook.value, hook.setValue];
  }

  useEffect(callback, dependencies) {
    const component = this.currentComponent;
    const index = this.hookIndex++;
    const componentHooks = this.hooks.get(component);
    
    const hook = componentHooks[index] || {
      cleanup: null,
      dependencies: null
    };
    
    const hasChanged = dependencies === undefined ||
      hook.dependencies === null ||
      dependencies.some((dep, i) => dep !== hook.dependencies[i]);
    
    if (hasChanged) {
      if (hook.cleanup) {
        hook.cleanup();
      }
      
      hook.cleanup = callback();
      hook.dependencies = dependencies ? [...dependencies] : null;
    }
    
    componentHooks[index] = hook;
  }

  useStore(store, selector = (state) => state) {
    const [state, setState] = this.useState(selector(store.getState()));
    
    this.useEffect(() => {
      return store.subscribe((newState) => {
        const selectedState = selector(newState);
        setState(selectedState);
      });
    }, [store, selector]);
    
    return state;
  }
}

// Usage example
const hooks = new VanillaHooks();

class TodoComponent {
  constructor(container, store) {
    this.container = container;
    this.store = store;
    this.render();
  }

  render() {
    hooks.withComponent(this, () => {
      const [newTodo, setNewTodo] = hooks.useState('');
      const todos = hooks.useStore(this.store, state => state.todos);
      
      hooks.useEffect(() => {
        console.log(`Todo count: ${todos.length}`);
      }, [todos.length]);
      
      this.container.innerHTML = `
        <div>
          <input 
            typ="text" 
            placeholde="Add todo..."
            valu="${newTodo}"
            onchang="this.component.updateNewTodo(this.value)"
          />
          <button onclic="this.component.addTodo()">Add</button>
          
          <ul>
            ${todos.map(todo => `
              <li>
                <span ${todo.completed ? 'style="text-decoration: line-through"' : ''}>
                  ${todo.text}
                </span>
                <button onclic="this.component.toggleTodo(${todo.id})">
                  ${todo.completed ? 'Undo' : 'Complete'}
                </button>
              </li>
            `).join('')}
          </ul>
        </div>
      `;
      
      // Store references for event handlers
      this.container.querySelectorAll('input, button').forEach(el => {
        el.component = this;
      });
      
      this.setNewTodo = setNewTodo;
    });
  }

  updateNewTodo(value) {
    this.setNewTodo(value);
  }

  addTodo() {
    hooks.withComponent(this, () => {
      const [newTodo] = hooks.useState('');
      if (newTodo.trim()) {
        this.store.dispatch({
          type: 'ADD_TODO',
          payload: { text: newTodo.trim() }
        });
        this.setNewTodo('');
      }
    });
  }

  toggleTodo(id) {
    this.store.dispatch({
      type: 'TOGGLE_TODO',
      payload: { id }
    });
  }
}

Conclusion

Vanilla JavaScript state management offers powerful alternatives to library-based solutions. Key benefits include:

Advantages:

  • No dependencies - Reduce bundle size and eliminate version conflicts
  • Full control - Customize behavior exactly to your needs
  • Performance - Optimize for your specific use cases
  • Learning - Deeper understanding of state management concepts
  • Flexibility - Easy integration with any framework or vanilla JS

When to Use Vanilla State Management:

  • Small to medium applications
  • Performance-critical applications
  • Learning projects
  • When you need specific behavior not available in libraries
  • Projects with strict dependency limitations

Implementation Strategy:

  1. Start simple - Begin with basic reactive state
  2. Add complexity gradually - Implement features as needed
  3. Focus on patterns - Use consistent patterns throughout your app
  4. Test thoroughly - Validate state logic with unit tests
  5. Document well - State management logic should be well-documented

Key Patterns to Remember:

  • Immutable updates prevent unexpected side effects
  • Event-driven architecture enables loose coupling
  • Computed properties optimize derived state calculations
  • Middleware pattern enables powerful extensibility
  • Path-based subscriptions optimize re-rendering

Start with a simple reactive state for your next project and gradually add features as your application grows. You might find that vanilla state management provides all the power you need without the complexity of external libraries.