Watch
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 }
): () => 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, uses polling to detect nested changes.immediate: Whether to invoke the callback immediately upon setup (defaults totrue).
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 (callback runs immediately by default)
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, watch executes the callback immediately when set up. You can disable this behavior:
const state = reactive({ count: 0 });
// Execute immediately (default)
watch(() => state.count, (value) => {
console.log(value); // Runs immediately with current value
});
// Don't execute immediately
watch(() => state.count, (value) => {
console.log(value); // Only runs when count changes
}, { immediate: false });
Deep Watching
For deep watching of nested objects, use the deep option. This uses polling to detect changes:
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 via polling
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 uses polling (every 100ms by default) and may have performance implications for large objects. Prefer shallow watching when possible.
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