Tips and Examples

This guide provides practical tips, patterns, and examples to help you use QuantaJS effectively in both core JavaScript and React applications.

Core Package Tips

1. Store Organization Patterns

Single Store for Small Apps

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

const useAppStore = defineStore('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: {
    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;
      }
    },
    addTodo(text) {
      this.todos.push({
        id: Date.now(),
        text,
        done: false,
        createdAt: new Date(),
      });
    },
    toggleTheme() {
      this.settings.theme = this.settings.theme === 'light' ? 'dark' : 'light';
    },
  },
});

Multiple Stores for Large Apps

// User store
const useUserStore = defineStore('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 useTodoStore = defineStore('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,
  },
  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,
      });
    },
  },
});

2. Performance Optimization

Selective Watching

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

const todos = useTodoStore();

// Good: Watch specific values
watch(() => todos.todos.length, (count) => {
  console.log(`Todo count: ${count}`);
});

// Avoid: Watching entire large objects
// watch(() => todos.todos, (list) => { ... }); // Expensive!

// Good: Watch computed summaries
watch(() => todos.todos.filter(todo => todo.done).length, (count) => {
  console.log(`${count} todos completed`);
});

Computed Caching

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

const useExpensiveStore = defineStore('expensive', {
  state: () => ({
    items: Array.from({ length: 1000 }, (_, i) => ({ id: i, value: Math.random() })),
  }),
  getters: {
    // This expensive calculation only runs when items change
    expensiveCalculation: (state) => {
      console.log('Computing expensive value...');
      return state.items
        .map(item => item.value * 2)
        .reduce((sum, val) => sum + val, 0);
    },
  },
});

const expensive = useExpensiveStore();

// First access - computes the value
console.log(expensive.expensiveCalculation);

// Second access - uses cached value (no computation)
console.log(expensive.expensiveCalculation);

// Only when items change does it recompute
expensive.items.push({ id: 1000, value: 0.5 });
console.log(expensive.expensiveCalculation); // Recomputes

3. Error Handling Patterns

const useErrorStore = defineStore('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;
      }
    },
    
    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 useUserStore = defineStore('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) {
        // Resolved at call time, not at module scope — a module-scope
        // resolution would bind to whichever container happened to be
        // ambient when this file first loaded.
        useErrorStore().addError(error, 'user.login');
      }
    },
  },
});

React Package Tips

1. Define once, resolve where you need it

// stores/counter.js
import { defineStore } from '@quantajs/core';

export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  getters: {
    doubleCount: (state) => state.count * 2,
  },
  actions: {
    increment() { this.count++; },
    decrement() { this.count--; },
  },
});
import { useQuanta } from '@quantajs/react';
import { useCounterStore } from './stores/counter';

function Counter() {
  const counter = useQuanta(useCounterStore);

  return (
    <div>
      <p>Count: {counter.count}</p>
      <p>Double: {counter.doubleCount}</p>
      <button onClick={() => counter.increment()}>+</button>
      <button onClick={() => counter.decrement()}>-</button>
    </div>
  );
}

No provider is needed in a client-only app. Add <QuantaProvider> when you want to control the container's lifetime — under SSR that is per request.

2. Subscribe to less

useQuanta wakes on any change to the store. useQuantaValue subscribes to exactly what the selector reads, so a component reading s.a is not re-rendered by a write to s.b.

import { useQuantaValue, useQuantaActions, shallow } from '@quantajs/react';

function TodoList() {
  const todos = useQuantaValue(useTodosStore, (s) => s.filteredTodos);

  return (
    <ul>
      {todos.map((todo) => (
        <TodoItem key={todo.id} todo={todo} />
      ))}
    </ul>
  );
}

function TodoStats() {
  // A selector returning a fresh object needs a comparator, or it
  // re-renders on every notification.
  const stats = useQuantaValue(
    useTodosStore,
    (s) => ({ completed: s.completedCount, pending: s.pendingCount }),
    { equalityFn: shallow },
  );

  return (
    <div>
      <p>Completed: {stats.completed}</p>
      <p>Pending: {stats.pending}</p>
    </div>
  );
}

/** Dispatch-only: resolves the store without subscribing, so it never
 *  re-renders on a state change. */
function AddTodoButton() {
  const todos = useQuantaActions(useTodosStore);
  return <button onClick={() => todos.add('New task')}>Add</button>;
}

Put expensive derivations in a getter:

A heavy computation inside a selector runs on every notification. Move it into the store as a getter — getters are cached and recompute only when something they read has changed — or use useComputed, which caches the same way.

// ❌ recomputed on every notification
const total = useQuantaValue(useCartStore, (s) =>
  s.items.filter((i) => i.active).reduce((n, i) => n + i.value, 0),
);

// ✅ cached in the store
getters: {
  activeTotal: (s) => s.items.filter((i) => i.active)
                             .reduce((n, i) => n + i.value, 0),
}
const total = useQuantaValue(useCartStore, (s) => s.activeTotal);

3. Component-scoped stores

useLocalStore gives each mount its own container, disposed on unmount, so two instances of the component never share state.

// stores/todo-widget.js
import { defineStore } from '@quantajs/core';

export const useTodoWidgetStore = defineStore('todo-widget', {
  state: () => ({ todos: [] }),
  getters: {
    activeCount: (state) => state.todos.filter((t) => !t.done).length,
  },
  actions: {
    addTodo(text) {
      this.todos.push({ id: crypto.randomUUID(), text, done: false });
    },
    toggleTodo(id) {
      const todo = this.todos.find((t) => t.id === id);
      if (todo) todo.done = !todo.done;
    },
  },
});
import { useLocalStore } from '@quantajs/react';

function TodoWidget() {
  const todos = useLocalStore(useTodoWidgetStore);

  return (
    <div>
      <button onClick={() => todos.addTodo('New task')}>Add Todo</button>
      <p>Todos: {todos.todos.length}</p>
      <p>Active: {todos.activeCount}</p>
    </div>
  );
}

// <TodoWidget /> <TodoWidget />  — independent lists.

This replaces useCreateStore, whose loose state / getters / actions arguments could not be typed properly.

4. Multiple stores

There is no store map to register. Each definition resolves into the container on first use.

// stores/index.js
export { useUserStore } from './user';
export { useTodosStore } from './todos';
import { useQuanta } from '@quantajs/react';
import { useUserStore, useTodosStore } from './stores';

function UserDashboard() {
  const user = useQuanta(useUserStore);
  const todos = useQuanta(useTodosStore);

  return (
    <div>
      <h1>{user.displayName}</h1>
      <p>{todos.todos.length} todos</p>
    </div>
  );
}

5. Custom hooks for store logic

function useUser() {
  const user = useQuanta(useUserStore);

  return {
    profile: user.profile,
    // The action lifecycle is built in — no `isLoading` field to maintain.
    isLoading: user.login.pending,
    error: user.login.error,
    login: user.login,
    logout: user.logout,
    isAuthenticated: !!user.profile,
  };
}

function UserProfile() {
  const { profile, isAuthenticated, isLoading, error, login } = useUser();

  if (isLoading) return <Spinner />;
  if (!isAuthenticated) return <LoginForm onLogin={login} error={error} />;

  return (
    <div>
      <h1>{profile.name}</h1>
      <TodoManager />
    </div>
  );
}

Note:

Resolve stores through hooks, never by calling useUserStore() inside a component. A direct call bypasses the provider and reads the ambient container, so under a per-request container it would hand you a different store than the rest of the page. useQuantaActions is the zero-subscription way to get the store from the right container.

Advanced Patterns

1. Form Handling

const useFormStore = defineStore('form', {
  state: () => ({
    fields: {
      name: '',
      email: '',
      password: '',
    },
    errors: {},
    isSubmitting: false,
  }),
  getters: {
    isValid: (state) => {
      return state.fields.name && 
             state.fields.email && 
             state.fields.password &&
             Object.keys(state.errors).length === 0;
    },
  },
  actions: {
    updateField(field, value) {
      this.fields[field] = value;
      this.validateField(field, value);
    },
    
    validateField(field, value) {
      const errors = {};
      
      if (field === 'email' && value && !value.includes('@')) {
        errors.email = 'Invalid email format';
      }
      
      if (field === 'password' && value && value.length < 6) {
        errors.password = 'Password must be at least 6 characters';
      }
      
      this.errors = { ...this.errors, ...errors };
    },
    
    async submit() {
      if (!this.isValid) return;
      
      this.isSubmitting = true;
      try {
        const response = await fetch('/api/register', {
          method: 'POST',
          body: JSON.stringify(this.fields),
        });
        
        if (!response.ok) {
          throw new Error('Registration failed');
        }
        
        // Handle success
      } catch (error) {
        console.error('Registration failed:', error);
      } finally {
        this.isSubmitting = false;
      }
    },
  },
});

2. Real-time Updates with WebSocket

const useChatStore = defineStore('chat', {
  state: () => ({
    messages: [],
    isConnected: false,
    socket: null,
  }),
  actions: {
    connect() {
      this.socket = new WebSocket('ws://localhost:8080/chat');
      
      this.socket.onopen = () => {
        this.isConnected = true;
      };
      
      this.socket.onmessage = (event) => {
        const message = JSON.parse(event.data);
        this.messages.push(message);
      };
      
      this.socket.onclose = () => {
        this.isConnected = false;
      };
    },
    
    sendMessage(text) {
      if (this.socket && this.isConnected) {
        this.socket.send(JSON.stringify({ text, timestamp: Date.now() }));
      }
    },
    
    disconnect() {
      if (this.socket) {
        this.socket.close();
        this.socket = null;
      }
    },
  },
});

3. Local Storage Synchronization

const useSettingsStore = defineStore('settings', {
  state: () => ({
    theme: 'light',
    language: 'en',
    notifications: { email: true, push: false },
  }),
  actions: {
    initialize() {
      // Load from localStorage
      const savedTheme = localStorage.getItem('theme');
      const savedLanguage = localStorage.getItem('language');
      const savedNotifications = localStorage.getItem('notifications');
      
      if (savedTheme) this.theme = savedTheme;
      if (savedLanguage) this.language = savedLanguage;
      if (savedNotifications) {
        try {
          this.notifications = JSON.parse(savedNotifications);
        } catch (error) {
          console.error('Failed to parse saved notifications:', error);
        }
      }
    },
    
    updateTheme(theme) {
      this.theme = theme;
      localStorage.setItem('theme', theme);
    },
    
    updateLanguage(language) {
      this.language = language;
      localStorage.setItem('language', language);
    },
    
    updateNotifications(notifications) {
      this.notifications = { ...this.notifications, ...notifications };
      localStorage.setItem('notifications', JSON.stringify(this.notifications));
    },
  },
});

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

Best Practices Summary

Core Package

  1. Use descriptive store names - Make them unique and meaningful
  2. Keep actions focused - Each action should do one thing well
  3. Leverage getters - Use computed values for derived state
  4. Handle errors gracefully - Always provide error handling in async actions
  5. Use watchers sparingly - Prefer computed values when possible

React Package

  1. Use selectors for performance - Prevent unnecessary re-renders
  2. Prefer QuantaProvider - For app-level state management
  3. Use component stores - For local state that doesn't need sharing
  4. Create custom hooks - Encapsulate store logic for reusability
  5. Keep it simple - Don't over-engineer your state management

General

  1. Start simple - Begin with a single store and split as needed
  2. Test your stores - Write unit tests for store logic
  3. Document your stores - Use TypeScript interfaces for better documentation
  4. Monitor performance - Use React DevTools and browser profilers
  5. Follow conventions - Be consistent with naming and structure

Learn More