Watch
These examples use `createStore`:
createStore builds a store instance immediately and is still fully supported.
In application code prefer defineStore, which
returns an accessor you call — it is safe at module scope on a server and needs
no container passed around. Everything on this page applies to both.
watch
Watches for changes in reactive state and executes a callback when changes occur.
Signature
function watch<T>(
source: () => T,
callback: (value: T, oldValue?: T) => void,
options?: {
deep?: boolean;
immediate?: boolean;
equals?: (value: T, oldValue: T) => boolean;
}
): () => void;
Parameters
source: A function that returns the value to watch. This function is automatically tracked for dependencies.callback: A function that executes when the watched value changes. Receives the new value and optionally the old value as parameters.options(optional): Configuration options:deep: Whether to perform deep watching (defaults tofalse). Whentrue, recursively tracks all nested properties for changes.immediate: Whether to invoke the callback once immediately upon setup (defaults tofalse).equals: Decides whether a re-run produced the same value; the callback fires only when it returnsfalse. Defaults toObject.is. Ignored withdeep.
Returns
A cleanup function that stops the watcher when called.
Basic Usage
import { reactive, watch } from '@quantajs/core';
const state = reactive({
count: 0,
name: 'Quanta',
});
// Watch a single value (the callback runs on the next change)
const unwatchCount = watch(() => state.count, (newValue, oldValue) => {
console.log(`Count changed from ${oldValue} to ${newValue}`);
});
// Watch another value
const unwatchName = watch(() => state.name, (newValue) => {
console.log(`Name changed to: ${newValue}`);
});
state.count = 5; // Logs: "Count changed from 0 to 5"
state.name = 'QuantaJS'; // Logs: "Name changed to: QuantaJS"
// Stop watching when done
unwatchCount();
unwatchName();
Watching Multiple Values
Watch multiple reactive values in a single watcher:
const user = reactive({
firstName: 'John',
lastName: 'Doe',
age: 25,
});
// Watch computed values
watch(() => `${user.firstName} ${user.lastName}`, (fullName) => {
console.log(`Full name changed to: ${fullName}`);
});
// Watch derived values
watch(() => user.age >= 18, (isAdult) => {
console.log(`User is now ${isAdult ? 'an adult' : 'a minor'}`);
});
// Watch multiple properties
watch(() => ({
name: `${user.firstName} ${user.lastName}`,
age: user.age,
isAdult: user.age >= 18,
}), (userInfo) => {
console.log('User info updated:', userInfo);
});
user.firstName = 'Jane'; // Triggers multiple watchers
user.age = 16; // Triggers age-related watchers
Watching Store State
Watch for changes in store state:
import { createStore } from '@quantajs/core';
const todoStore = createStore('todos', {
state: () => ({
todos: [],
filter: 'all',
}),
actions: {
addTodo(text) {
this.todos.push({ id: Date.now(), text, done: false });
},
setFilter(filter) {
this.filter = filter;
},
},
});
// Watch the entire todos array
watch(() => todoStore.todos, (newTodos) => {
console.log(`Todos updated: ${newTodos.length} items`);
localStorage.setItem('todos', JSON.stringify(newTodos));
});
// Watch specific computed values
watch(() => todoStore.todos.filter(t => t.done).length, (completedCount) => {
console.log(`${completedCount} todos completed`);
});
// Watch filter changes
watch(() => todoStore.filter, (newFilter) => {
console.log(`Filter changed to: ${newFilter}`);
});
todoStore.addTodo('Learn QuantaJS');
todoStore.addTodo('Build an app');
todoStore.setFilter('active');
Side Effects with Watch
Use watch for common side effects like API calls, DOM updates, and logging:
const userStore = createStore('user', {
state: () => ({
user: null,
isLoading: false,
userId: null,
}),
actions: {
setUserId(id) {
this.userId = id;
},
},
});
// Watch for user ID changes and fetch user data
watch(() => userStore.userId, async (userId) => {
if (userId) {
userStore.isLoading = true;
try {
const response = await fetch(`/api/users/${userId}`);
userStore.user = await response.json();
} catch (error) {
console.error('Failed to fetch user:', error);
} finally {
userStore.isLoading = false;
}
} else {
userStore.user = null;
}
});
// Watch for user changes and update page title
watch(() => userStore.user?.name, (userName) => {
if (userName) {
document.title = `Profile - ${userName}`;
} else {
document.title = 'User Profile';
}
});
// Watch for loading state changes
watch(() => userStore.isLoading, (isLoading) => {
const spinner = document.getElementById('spinner');
if (spinner) {
spinner.style.display = isLoading ? 'block' : 'none';
}
});
// Watch for authentication state
watch(() => !!userStore.user, (isAuthenticated) => {
if (isAuthenticated) {
console.log('User authenticated, redirecting to dashboard');
// router.push('/dashboard');
} else {
console.log('User logged out, redirecting to login');
// router.push('/login');
}
});
userStore.setUserId(123); // Triggers user fetch
Watching Nested Objects
Watch deeply nested object changes:
const settings = reactive({
theme: {
mode: 'dark',
colors: {
primary: '#007bff',
secondary: '#6c757d',
},
},
notifications: {
email: true,
push: false,
},
});
// Watch specific nested properties
watch(() => settings.theme.mode, (newMode) => {
document.body.setAttribute('data-theme', newMode);
});
// Watch multiple nested properties
watch(() => ({
mode: settings.theme.mode,
primary: settings.theme.colors.primary,
}), (newSettings) => {
console.log('Theme settings changed:', newSettings);
});
// Watch entire nested objects
watch(() => settings.notifications, (newNotifications) => {
console.log('Notification settings updated:', newNotifications);
});
settings.theme.mode = 'light';
settings.theme.colors.primary = '#28a745';
settings.notifications.push = true;
Performance Considerations
Watch can be expensive for large objects. Use specific selectors when possible:
const largeStore = createStore('large', {
state: () => ({
items: Array.from({ length: 1000 }, (_, i) => ({ id: i, value: Math.random() })),
metadata: { /* large object */ },
}),
});
// Good: Watch specific values
watch(() => largeStore.items.length, (count) => {
console.log(`Item count: ${count}`);
});
// Avoid: Watching entire large objects
// watch(() => largeStore.items, (items) => { ... }); // Expensive!
// Good: Watch computed summaries
watch(() => largeStore.items.filter(item => item.value > 0.5).length, (count) => {
console.log(`${count} items above threshold`);
});
// Good: Watch specific properties
watch(() => largeStore.metadata.lastUpdated, (timestamp) => {
console.log(`Last updated: ${timestamp}`);
});
Common Use Cases
Form Validation
const form = reactive({
email: '',
password: '',
confirmPassword: '',
});
// Watch for email changes and validate
watch(() => form.email, (email) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const isValid = emailRegex.test(email);
if (!isValid && email) {
console.log('Invalid email format');
}
});
// Watch for password confirmation
watch(() => [form.password, form.confirmPassword], ([password, confirmPassword]) => {
if (password && confirmPassword && password !== confirmPassword) {
console.log('Passwords do not match');
}
});
Debounced Search
const searchStore = reactive({
query: '',
results: [],
isLoading: false,
});
let searchTimeout;
watch(() => searchStore.query, (query) => {
clearTimeout(searchTimeout);
if (query.length > 2) {
searchTimeout = setTimeout(async () => {
searchStore.isLoading = true;
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
searchStore.results = await response.json();
} catch (error) {
console.error('Search failed:', error);
} finally {
searchStore.isLoading = false;
}
}, 300);
} else {
searchStore.results = [];
}
});
Local Storage Sync
const appState = reactive({
theme: 'light',
language: 'en',
preferences: {
notifications: true,
autoSave: true,
},
});
// Sync theme to localStorage
watch(() => appState.theme, (theme) => {
localStorage.setItem('theme', theme);
});
// Sync language to localStorage
watch(() => appState.language, (language) => {
localStorage.setItem('language', language);
});
// Sync all preferences with deep watching
const unwatchPreferences = watch(() => appState.preferences, (preferences) => {
localStorage.setItem('preferences', JSON.stringify(preferences));
}, { deep: true });
// Clean up when component unmounts
// unwatchPreferences();
Watch Options
Immediate Execution
By default the callback runs only when the value changes. Pass immediate: true to also run it once when the watcher is set up:
const state = reactive({ count: 0 });
// Only runs when count changes (default)
watch(() => state.count, (value) => {
console.log(value);
});
// Also runs immediately with the current value
watch(() => state.count, (value) => {
console.log(value);
}, { immediate: true });
Deep Watching
For deep watching of nested objects, use the deep option. This recursively tracks all nested properties using reactive dependency tracking:
const state = reactive({
user: {
profile: {
name: 'John',
email: 'john@example.com',
},
},
});
// Shallow watch (default) - only detects direct property changes
watch(() => state.user, (user) => {
console.log('User object reference changed');
});
// Deep watch - detects nested property changes reactively
const unwatch = watch(() => state.user, (user) => {
console.log('User or nested properties changed');
}, { deep: true });
// Changes to nested properties will trigger the callback
state.user.profile.name = 'Jane'; // Triggers deep watch
// Clean up when done
unwatch();
Note: Deep watching recursively accesses all nested properties to register reactive dependencies. For very large or deeply nested objects, prefer watching specific properties for better performance.
Custom Equality
The callback fires when the source returns a value that is not Object.is-equal to the previous one. A source that builds a new object on each run is never equal, so pass shallow to compare it one level deep:
import { reactive, shallow, watch } from '@quantajs/core';
const form = reactive({ errors: [], touched: [] });
// Re-runs whenever either list changes, but fires only when
// `valid` or `dirty` actually flips.
watch(
() => ({ valid: form.errors.length === 0, dirty: form.touched.length > 0 }),
(status) => updateSubmitButton(status),
{ equals: shallow },
);
The opposite also works: equals: () => false fires on every change the source tracks, including an in-place mutation of an object it returns by identity.
Cleanup
Watch returns a cleanup function that stops the watcher:
const state = reactive({ count: 0 });
const unwatch = watch(() => state.count, (value) => {
console.log(value);
});
// Later, stop watching
unwatch();
// After unwatch, changes won't trigger the callback
state.count = 5; // No log output
Learn More
- Watching State Guide - Understanding watchers
- Reactive State - Reactive fundamentals
- Computed Values - Derived state
- React Integration - React applications
Spot something that needs improving?
Edit on GitHubEdit page