Managing Stores

QuantaJS provides powerful tools for managing state across your application. This guide covers best practices for organizing, creating, and using stores effectively.

Store Organization

Single Store Pattern

For smaller applications, a single store can manage all state:

import { createStore } from '@quantajs/core';

const appStore = createStore('app', {
  state: () => ({
    user: null,
    todos: [],
    settings: {
      theme: 'light',
      notifications: true,
    },
    ui: {
      isLoading: false,
      sidebarOpen: false,
    },
  }),
  getters: {
    isAuthenticated: (state) => !!state.user,
    completedTodos: (state) => state.todos.filter(todo => todo.done),
    pendingTodos: (state) => state.todos.filter(todo => !todo.done),
  },
  actions: {
    // User actions
    async login(credentials) {
      this.ui.isLoading = true;
      try {
        const response = await fetch('/api/login', {
          method: 'POST',
          body: JSON.stringify(credentials),
        });
        this.user = await response.json();
      } catch (error) {
        console.error('Login failed:', error);
      } finally {
        this.ui.isLoading = false;
      }
    },
    logout() {
      this.user = null;
      this.todos = [];
    },

    // Todo actions
    addTodo(text) {
      this.todos.push({
        id: Date.now(),
        text,
        done: false,
        createdAt: new Date(),
      });
    },
    toggleTodo(id) {
      const todo = this.todos.find(t => t.id === id);
      if (todo) todo.done = !todo.done;
    },
    removeTodo(id) {
      this.todos = this.todos.filter(t => t.id !== id);
    },

    // Settings actions
    updateSettings(updates) {
      Object.assign(this.settings, updates);
    },
    toggleTheme() {
      this.settings.theme = this.settings.theme === 'light' ? 'dark' : 'light';
    },

    // UI actions
    setLoading(isLoading) {
      this.ui.isLoading = isLoading;
    },
    toggleSidebar() {
      this.ui.sidebarOpen = !this.ui.sidebarOpen;
    },
  },
});

Multiple Stores Pattern

For larger applications, split state into domain-specific stores:

// User store
const userStore = createStore('user', {
  state: () => ({
    user: null,
    isLoading: false,
    error: null,
  }),
  getters: {
    isAuthenticated: (state) => !!state.user,
    userRole: (state) => state.user?.role || 'guest',
  },
  actions: {
    async login(credentials) {
      this.isLoading = true;
      this.error = null;
      try {
        const response = await fetch('/api/login', {
          method: 'POST',
          body: JSON.stringify(credentials),
        });
        this.user = await response.json();
      } catch (error) {
        this.error = error.message;
      } finally {
        this.isLoading = false;
      }
    },
    logout() {
      this.user = null;
      this.error = null;
    },
  },
});

// Todo store
const todoStore = createStore('todos', {
  state: () => ({
    todos: [],
    filter: 'all',
    isLoading: false,
  }),
  getters: {
    filteredTodos: (state) => {
      switch (state.filter) {
        case 'active':
          return state.todos.filter(todo => !todo.done);
        case 'completed':
          return state.todos.filter(todo => todo.done);
        default:
          return state.todos;
      }
    },
    completedCount: (state) => state.todos.filter(todo => todo.done).length,
    pendingCount: (state) => state.todos.filter(todo => !todo.done).length,
  },
  actions: {
    async fetchTodos() {
      this.isLoading = true;
      try {
        const response = await fetch('/api/todos');
        this.todos = await response.json();
      } catch (error) {
        console.error('Failed to fetch todos:', error);
      } finally {
        this.isLoading = false;
      }
    },
    addTodo(text) {
      this.todos.push({
        id: Date.now(),
        text,
        done: false,
        createdAt: new Date(),
      });
    },
    toggleTodo(id) {
      const todo = this.todos.find(t => t.id === id);
      if (todo) todo.done = !todo.done;
    },
    setFilter(filter) {
      this.filter = filter;
    },
  },
});

// Settings store
const settingsStore = createStore('settings', {
  state: () => ({
    theme: 'light',
    language: 'en',
    notifications: {
      email: true,
      push: false,
    },
  }),
  actions: {
    updateTheme(theme) {
      this.theme = theme;
      localStorage.setItem('theme', theme);
    },
    updateLanguage(language) {
      this.language = language;
      localStorage.setItem('language', language);
    },
    toggleNotification(type) {
      this.notifications[type] = !this.notifications[type];
    },
  },
});

Store Communication

Cross-Store Actions

Stores can communicate with each other through actions:

const userStore = createStore('user', {
  state: () => ({ user: null }),
  actions: {
    async login(credentials) {
      // ... login logic
      this.user = userData;
      
      // Notify other stores
      todoStore.fetchTodos();
      settingsStore.loadUserPreferences(userData.id);
    },
    logout() {
      this.user = null;
      
      // Clear other stores
      todoStore.clearTodos();
      settingsStore.resetToDefaults();
    },
  },
});

const todoStore = createStore('todos', {
  state: () => ({ todos: [] }),
  actions: {
    clearTodos() {
      this.todos = [];
    },
    async fetchTodos() {
      if (!userStore.user) return;
      
      const response = await fetch(`/api/users/${userStore.user.id}/todos`);
      this.todos = await response.json();
    },
  },
});

Shared State

For shared state between stores, create a shared store:

const sharedStore = createStore('shared', {
  state: () => ({
    isLoading: false,
    error: null,
    notifications: [],
  }),
  actions: {
    setLoading(isLoading) {
      this.isLoading = isLoading;
    },
    setError(error) {
      this.error = error;
    },
    addNotification(message, type = 'info') {
      this.notifications.push({
        id: Date.now(),
        message,
        type,
        timestamp: new Date(),
      });
    },
    removeNotification(id) {
      this.notifications = this.notifications.filter(n => n.id !== id);
    },
  },
});

// Other stores can access shared state
const userStore = createStore('user', {
  state: () => ({ user: null }),
  actions: {
    async login(credentials) {
      sharedStore.setLoading(true);
      sharedStore.setError(null);
      
      try {
        const response = await fetch('/api/login', {
          method: 'POST',
          body: JSON.stringify(credentials),
        });
        this.user = await response.json();
        sharedStore.addNotification('Login successful!', 'success');
      } catch (error) {
        sharedStore.setError(error.message);
        sharedStore.addNotification('Login failed', 'error');
      } finally {
        sharedStore.setLoading(false);
      }
    },
  },
});

Store Lifecycle

Initialization

Initialize stores with default values and load persisted data:

const appStore = createStore('app', {
  state: () => ({
    user: null,
    settings: {
      theme: 'light',
      language: 'en',
    },
  }),
  actions: {
    initialize() {
      // Load persisted settings
      const savedTheme = localStorage.getItem('theme');
      const savedLanguage = localStorage.getItem('language');
      
      if (savedTheme) this.settings.theme = savedTheme;
      if (savedLanguage) this.settings.language = savedLanguage;
      
      // Check for saved user session
      const savedUser = localStorage.getItem('user');
      if (savedUser) {
        try {
          this.user = JSON.parse(savedUser);
        } catch (error) {
          localStorage.removeItem('user');
        }
      }
    },
    
    persistSettings() {
      localStorage.setItem('theme', this.settings.theme);
      localStorage.setItem('language', this.settings.language);
    },
    
    persistUser() {
      if (this.user) {
        localStorage.setItem('user', JSON.stringify(this.user));
      } else {
        localStorage.removeItem('user');
      }
    },
  },
});

// Initialize on app start
appStore.initialize();

Cleanup

Clean up stores when components unmount or when needed:

const todoStore = createStore('todos', {
  state: () => ({ todos: [] }),
  actions: {
    cleanup() {
      this.todos = [];
      // Cancel any pending requests
      if (this.currentRequest) {
        this.currentRequest.abort();
      }
    },
  },
});

// In React component
useEffect(() => {
  return () => {
    todoStore.cleanup();
  };
}, []);

Performance Optimization

Selective Updates

Use selectors to prevent unnecessary re-renders:

// In React components
function TodoList() {
  // Only re-render when todos change
  const todos = useQuantaStore(todoStore, store => store.filteredTodos);
  
  return (
    <ul>
      {todos.map(todo => (
        <TodoItem key={todo.id} todo={todo} />
      ))}
    </ul>
  );
}

function TodoStats() {
  // Only re-render when counts change
  const stats = useQuantaStore(todoStore, store => ({
    completed: store.completedCount,
    pending: store.pendingCount,
  }));
  
  return (
    <div>
      <p>Completed: {stats.completed}</p>
      <p>Pending: {stats.pending}</p>
    </div>
  );
}

Lazy Loading

Load data only when needed:

const userStore = createStore('user', {
  state: () => ({
    user: null,
    profile: null,
    isLoading: false,
  }),
  actions: {
    async loadProfile() {
      if (this.profile || !this.user) return;
      
      this.isLoading = true;
      try {
        const response = await fetch(`/api/users/${this.user.id}/profile`);
        this.profile = await response.json();
      } catch (error) {
        console.error('Failed to load profile:', error);
      } finally {
        this.isLoading = false;
      }
    },
  },
});

Error Handling

Centralized Error Handling

Create a centralized error handling system:

const errorStore = createStore('errors', {
  state: () => ({
    errors: [],
    globalError: null,
  }),
  actions: {
    addError(error, context = '') {
      const errorInfo = {
        id: Date.now(),
        message: error.message || error,
        context,
        timestamp: new Date(),
        stack: error.stack,
      };
      
      this.errors.push(errorInfo);
      this.globalError = errorInfo;
      
      // Log to external service
      this.logError(errorInfo);
    },
    
    clearError(id) {
      this.errors = this.errors.filter(e => e.id !== id);
      if (this.globalError?.id === id) {
        this.globalError = null;
      }
    },
    
    clearAllErrors() {
      this.errors = [];
      this.globalError = null;
    },
    
    async logError(errorInfo) {
      try {
        await fetch('/api/errors', {
          method: 'POST',
          body: JSON.stringify(errorInfo),
        });
      } catch (error) {
        console.error('Failed to log error:', error);
      }
    },
  },
});

// Use in other stores
const userStore = createStore('user', {
  state: () => ({ user: null }),
  actions: {
    async login(credentials) {
      try {
        const response = await fetch('/api/login', {
          method: 'POST',
          body: JSON.stringify(credentials),
        });
        
        if (!response.ok) {
          throw new Error('Login failed');
        }
        
        this.user = await response.json();
      } catch (error) {
        errorStore.addError(error, 'user.login');
      }
    },
  },
});

Testing Stores

Unit Testing

Test stores in isolation:

import { createStore } from '@quantajs/core';

describe('Todo Store', () => {
  let todoStore;
  
  beforeEach(() => {
    todoStore = createStore('test-todos', {
      state: () => ({ todos: [] }),
      actions: {
        addTodo(text) {
          this.todos.push({ id: Date.now(), text, done: false });
        },
        toggleTodo(id) {
          const todo = this.todos.find(t => t.id === id);
          if (todo) todo.done = !todo.done;
        },
      },
    });
  });
  
  test('should add todo', () => {
    todoStore.addTodo('Test todo');
    expect(todoStore.todos).toHaveLength(1);
    expect(todoStore.todos[0].text).toBe('Test todo');
  });
  
  test('should toggle todo', () => {
    todoStore.addTodo('Test todo');
    const todo = todoStore.todos[0];
    
    expect(todo.done).toBe(false);
    todoStore.toggleTodo(todo.id);
    expect(todo.done).toBe(true);
  });
});

Learn More