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
- Use descriptive store names - Make them unique and meaningful
- Keep actions focused - Each action should do one thing well
- Leverage getters - Use computed values for derived state
- Handle errors gracefully - Always provide error handling in async actions
- Use watchers sparingly - Prefer computed values when possible
React Package
- Use selectors for performance - Prevent unnecessary re-renders
- Prefer QuantaProvider - For app-level state management
- Use component stores - For local state that doesn't need sharing
- Create custom hooks - Encapsulate store logic for reusability
- Keep it simple - Don't over-engineer your state management
General
- Start simple - Begin with a single store and split as needed
- Test your stores - Write unit tests for store logic
- Document your stores - Use TypeScript interfaces for better documentation
- Monitor performance - Use React DevTools and browser profilers
- Follow conventions - Be consistent with naming and structure
Learn More
- Reactive State - Understanding reactivity
- Computed Values - Derived state
- Watching State - Side effects
- React Integration - React applications
- Managing Stores - Store management patterns