Watch
watch
Watches for changes in reactive state and executes a callback when changes occur.
Signature
function watch<T>(source: () => T, callback: (value: T) => void): 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 as a parameter.
Returns
void
- The watch function doesn't return anything.
Basic Usage
import { reactive, watch } from '@quantajs/core';
const state = reactive({
count: 0,
name: 'Quanta',
});
// Watch a single value
watch(() => state.count, (newValue) => {
console.log(`Count changed to: ${newValue}`);
});
// Watch another value
watch(() => state.name, (newValue) => {
console.log(`Name changed to: ${newValue}`);
});
state.count = 5; // Logs: "Count changed to: 5"
state.name = 'QuantaJS'; // Logs: "Name changed to: QuantaJS"
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
watch(() => appState.preferences, (preferences) => {
localStorage.setItem('preferences', JSON.stringify(preferences));
}, { deep: true });
Limitations
No Cleanup Function
The current watch implementation doesn't return a cleanup function:
// ❌ This doesn't work as expected
const unwatch = watch(() => state.count, (value) => {
console.log(value);
});
// No cleanup function available
// unwatch(); // This doesn't exist
No Immediate Option
Watch doesn't have an immediate option like some other libraries:
// ❌ No immediate option
// watch(() => state.count, callback, { immediate: true });
// ✅ Use a separate call for immediate execution
const callback = (value) => console.log(value);
callback(state.count); // Execute immediately
watch(() => state.count, callback); // Then watch for changes
Learn More
- Watching State Guide - Understanding watchers
- Reactive State - Reactive fundamentals
- Computed Values - Derived state
- React Integration - React applications