Understanding notify() - A Beginner's Guide
Quick Start (30 seconds)
Need to manually trigger effects without changing state? Here's how:
const app = state({ count: 0 });
effect(() => {
console.log('Count:', app.count);
});
// Logs: "Count: 0"
// Manually trigger effects for 'count' property
notify(app, 'count');
// Logs: "Count: 0" (again, without changing value)
// Trigger effects for all properties
notify(app);
// Logs: "Count: 0" (again)That's it! The notify() function manually triggers effects for specific properties or all properties!
What is notify()?
notify() is a manual notification function that triggers effects for a specific property or all properties of reactive state, even without changing the values. It lets you manually fire the reactive system's dependency notifications.
Manual notifications:
- Triggers effects without changing state
- Can notify for specific properties
- Can notify for all properties at once
- Useful for external changes or forced updates
- Queues effects like normal state changes
Think of it as ringing the doorbell manually - you're alerting everyone even though nothing changed.
Syntax
// Using the shortcut
notify(state, key)
// Using the full namespace
ReactiveUtils.notify(state, key)All styles are valid! Choose whichever you prefer:
- Shortcut style (
notify()) - Clean and concise - Namespace style (
ReactiveUtils.notify()) - Explicit and clear
Parameters:
state- The reactive state object (required for non-instance calls)key- The property key to notify (optional)- If provided: notifies only effects depending on that property
- If omitted: notifies all effects depending on any property
Returns:
- Nothing (undefined)
Why Does This Exist?
The Problem with External Changes
Let's say you have state that changes externally (outside the reactive system):
const canvas = state({
width: 800,
height: 600,
context: null
});
effect(() => {
// Render something based on canvas dimensions
console.log(`Rendering ${canvas.width}x${canvas.height} canvas`);
renderCanvas(canvas);
});
// Logs: "Rendering 800x600 canvas"
// External library modifies canvas size directly
const ctx = document.getElementById('canvas').getContext('2d');
ctx.canvas.width = 1024; // Changed externally!
ctx.canvas.height = 768; // Changed externally!
// ❌ Reactive system doesn't know about this change!
// Effect doesn't re-run
// UI is out of sync!The problem: changes made outside the reactive system don't trigger effects!
What's the Real Issue?
External Changes:
┌──────────────────┐
│ External system │
│ modifies data │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Direct mutation │
│ bypasses proxy │
└────────┬─────────┘
│
▼
[SILENCE] 🔇
│
▼
Effects don't run
UI out of sync!
State inconsistent!Problems: ❌ External changes bypass reactive tracking ❌ Effects don't know data changed ❌ UI becomes out of sync ❌ No way to manually trigger updates ❌ Can't force re-computation ❌ Inconsistent state representation
Why This Becomes a Problem:
Sometimes you need to:
- Integrate with external libraries that modify data
- Force re-render after external changes
- Manually trigger computed property recalculation
- Notify effects after batch external operations
- Debug reactivity by forcing updates
The Solution with notify()
When you use notify(), you can manually trigger effects:
const canvas = state({
width: 800,
height: 600,
context: null
});
effect(() => {
console.log(`Rendering ${canvas.width}x${canvas.height} canvas`);
renderCanvas(canvas);
});
// Logs: "Rendering 800x600 canvas"
// External library modifies canvas
const ctx = document.getElementById('canvas').getContext('2d');
ctx.canvas.width = 1024;
ctx.canvas.height = 768;
// Update reactive state to match
canvas.width = ctx.canvas.width;
canvas.height = ctx.canvas.height;
// Or, if you can't update state directly, manually notify
notify(canvas, 'width');
notify(canvas, 'height');
// Logs: "Rendering 1024x768 canvas"
// UI updates!What Just Happened?
With notify():
┌──────────────────┐
│ External change │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ notify(state, │
│ 'key') │
└────────┬─────────┘
│
▼
Triggers effects
manually! 🔔
│
▼
Effects re-run
UI updates!
State synced!With notify():
- Manually trigger reactive notifications
- Force effects to re-run
- Sync UI after external changes
- Control reactivity timing
- Force re-computation of derived values
Benefits: ✅ Manually trigger effects without state changes ✅ Integrate with external libraries ✅ Force UI updates when needed ✅ Sync state with external systems ✅ Debug reactivity issues ✅ Control notification timing
Mental Model
Think of notify() like ringing a doorbell manually:
Normal State Change (Doorbell Pressed):
┌──────────────────┐
│ Someone arrives │ ← State changes
│ Presses doorbell │
└────────┬─────────┘
│
▼
🔔 Ring!
│
▼
Everyone notified
Effects run
Manual Notify (Ring Without Visitor):
┌──────────────────┐
│ You manually │ ← No state change
│ press doorbell │
└────────┬─────────┘
│
▼
🔔 Ring!
│
▼
Everyone notified
Effects run
(Even though nobody arrived)Key Insight: Just like you can ring a doorbell even when nobody's at the door, notify() lets you trigger reactive notifications even when state hasn't changed.
How Does It Work?
The Magic: Direct Dependency Notification
When you call notify(), here's what happens behind the scenes:
// What you write:
notify(app, 'count');
// What actually happens (simplified):
// Reactive state stores dependencies for each property
const reactiveMap = new Map(); // Maps state to metadata
function notify(state, key) {
// Get metadata for this state
const meta = reactiveMap.get(state);
if (!meta) return;
if (key) {
// Notify effects for specific key
const effects = meta.deps.get(key);
if (effects) {
effects.forEach(effect => {
if (effect && !effect.isComputed) {
queueUpdate(effect); // Queue effect
}
});
}
} else {
// Notify effects for ALL keys
meta.deps.forEach(effects => {
effects.forEach(effect => {
if (effect && !effect.isComputed) {
queueUpdate(effect);
}
});
});
}
}In other words: notify():
- Gets the metadata for the reactive state
- If key provided: finds effects depending on that key
- If no key: finds effects depending on any key
- Queues all found effects for execution
- Effects run in next batch flush
Under the Hood
notify(state, 'count'):
┌──────────────────┐
│ Get state │
│ metadata │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Get effects │
│ for 'count' │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Queue each │
│ effect │
└────────┬─────────┘
│
▼
Effects run!
notify(state):
┌──────────────────┐
│ Get state │
│ metadata │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Get effects │
│ for ALL keys │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Queue all │
│ effects │
└────────┬─────────┘
│
▼
All effects run!What happens:
1️⃣ Retrieves state metadata 2️⃣ Finds effects depending on key (or all keys) 3️⃣ Queues effects for execution 4️⃣ Runs effects in next batch
Basic Usage
Notifying Specific Property
The simplest way to use notify():
const app = state({ count: 0, name: 'App' });
effect(() => {
console.log('Count:', app.count);
});
effect(() => {
console.log('Name:', app.name);
});
// Trigger only count effects
notify(app, 'count');
// Logs: "Count: 0"Notifying All Properties
Trigger all effects:
const app = state({ count: 0, name: 'App' });
effect(() => {
console.log('Count:', app.count);
});
effect(() => {
console.log('Name:', app.name);
});
// Trigger ALL effects
notify(app);
// Logs: "Count: 0"
// Logs: "Name: App"Using Instance Method
Object-oriented style:
const app = state({ count: 0 });
effect(() => {
console.log('Count:', app.count);
});
// Using instance method
notify('count');
// Logs: "Count: 0"
// Notify all
notify(app);
// Logs: "Count: 0"Notifying Specific Keys vs All Keys
Specific Key Notification
When you provide a key, only effects depending on that property run:
const app = state({
count: 0,
name: 'App',
theme: 'dark'
});
effect(() => console.log('Count:', app.count));
effect(() => console.log('Name:', app.name));
effect(() => console.log('Theme:', app.theme));
// Only count effect runs
notify(app, 'count');
// Logs: "Count: 0"All Keys Notification
When you omit the key, ALL effects run:
const app = state({
count: 0,
name: 'App',
theme: 'dark'
});
effect(() => console.log('Count:', app.count));
effect(() => console.log('Name:', app.name));
effect(() => console.log('Theme:', app.theme));
// All effects run
notify(app);
// Logs: "Count: 0"
// Logs: "Name: App"
// Logs: "Theme: dark"Multi-Dependency Effects
Effects with multiple dependencies:
const app = state({ firstName: 'John', lastName: 'Doe' });
effect(() => {
console.log('Full name:', `${app.firstName} ${app.lastName}`);
});
// Notify only firstName
notify(app, 'firstName');
// Logs: "Full name: John Doe" (effect has both dependencies)
// Notify only lastName
notify(app, 'lastName');
// Logs: "Full name: John Doe" (effect runs again)
// Notify all
notify(app);
// Logs: "Full name: John Doe" (only runs once, not twice)When to Use notify()
✅ Good Use Cases
1. External Library Integration
const canvas = state({ width: 800, height: 600 });
// External library changes canvas size
function resizeCanvas(newWidth, newHeight) {
const ctx = document.getElementById('canvas').getContext('2d');
ctx.canvas.width = newWidth;
ctx.canvas.height = newHeight;
// Notify reactive system
canvas.width = newWidth;
canvas.height = newHeight;
notify(canvas, 'width');
notify(canvas, 'height');
}2. Force Recomputation
const calc = state({ a: 1, b: 2 });
computed(calc, {
sum() { return this.a + this.b; }
});
// Force recompute even if values haven't changed
notify(calc, 'a');
notify(calc, 'b');3. Manual Refresh
function refreshUI() {
// Force all effects to re-run
notify(app);
}4. Sync with External State
function syncWithServer(serverData) {
// Update local state
Object.assign(app, serverData);
// Notify all to ensure everything updates
notify(app);
}❌ Not Needed
1. Normal State Changes
// Don't use notify for normal updates
❌ app.count = 5;
notify(app, 'count'); // Redundant!
// Just change the value
✅ app.count = 5; // Automatically notifies2. Multiple Updates
// Don't notify after each change
❌ batch(() => {
app.count = 1;
notify(app, 'count');
app.count = 2;
notify(app, 'count');
});
// Just change values
✅ batch(() => {
app.count = 1;
app.count = 2;
});Real-World Examples
Example 1: Canvas Integration
const canvasState = state({
width: 800,
height: 600,
zoom: 1,
isDirty: false
});
effect(() => {
if (canvasState.isDirty) {
renderCanvas(canvasState);
canvasState.isDirty = false;
}
});
// External resize handler
window.addEventListener('resize', () => {
const canvas = document.getElementById('canvas');
// Get new dimensions
const rect = canvas.getBoundingClientRect();
// Update state
canvasState.width = rect.width;
canvasState.height = rect.height;
canvasState.isDirty = true;
// Force update
notify(canvasState, 'isDirty');
});Example 2: WebSocket Integration
const chatState = state({
messages: [],
users: [],
connectionStatus: 'disconnected'
});
effect(() => {
renderMessages(chatState.messages);
});
effect(() => {
renderUserList(chatState.users);
});
// WebSocket handlers
const socket = new WebSocket('ws://localhost:3000');
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'message') {
chatState.messages.push(data.message);
notify(chatState, 'messages');
}
if (data.type === 'userJoined') {
chatState.users.push(data.user);
notify(chatState, 'users');
}
};
socket.onopen = () => {
chatState.connectionStatus = 'connected';
notify(chatState, 'connectionStatus');
};Example 3: Polling System
const dataState = state({
lastFetch: null,
data: null,
isStale: false
});
effect(() => {
if (dataState.isStale) {
console.log('Data is stale, needs refresh');
showRefreshButton();
}
});
// Poll for updates
setInterval(async () => {
const response = await fetch('/api/check-updates');
const { lastModified } = await response.json();
if (lastModified > dataState.lastFetch) {
dataState.isStale = true;
notify(dataState, 'isStale'); // Force effect
}
}, 30000); // Check every 30 seconds
// Manual refresh
function refreshData() {
dataState.isStale = false;
fetchLatestData().then(data => {
dataState.data = data;
dataState.lastFetch = Date.now();
});
}Example 4: Game Loop
const gameState = state({
players: [],
enemies: [],
score: 0,
level: 1,
frame: 0
});
effect(() => {
renderGame(gameState);
});
// Game loop
function gameLoop() {
// Update game logic
updatePlayers(gameState.players);
updateEnemies(gameState.enemies);
checkCollisions(gameState);
// Increment frame
gameState.frame++;
// Manually notify to trigger render
notify(gameState);
requestAnimationFrame(gameLoop);
}
gameLoop();Example 5: Form Validation on Submit
const formState = state({
email: '',
password: '',
errors: {},
isValidated: false
});
effect(() => {
if (formState.isValidated) {
// Run validation
const errors = {};
if (!formState.email.includes('@')) {
errors.email = 'Invalid email';
}
if (formState.password.length < 6) {
errors.password = 'Too short';
}
formState.errors = errors;
formState.isValidated = false;
}
});
function handleSubmit() {
// Trigger validation
formState.isValidated = true;
notify(formState, 'isValidated');
if (Object.keys(formState.errors).length === 0) {
submitForm(formState);
}
}Common Patterns
Pattern: Force Update
function forceUpdate(state) {
notify(state); // Notify all effects
}Pattern: Selective Notification
function notifyMultiple(state, keys) {
keys.forEach(key => {
notify(state, key);
});
}
// Usage
notifyMultiple(app, ['count', 'name', 'theme']);Pattern: External Sync
function syncExternal(state, externalData) {
batch(() => {
Object.assign(state, externalData);
});
notify(state); // Ensure everything updates
}Pattern: Debug Trigger
function debugTrigger(state, key) {
console.log(`[Debug] Manually triggering effects for "${key}"`);
notify(state, key);
}Common Pitfalls
Pitfall #1: Notifying After Normal Changes
❌ Wrong:
app.count = 5;
notify(app, 'count'); // Redundant!✅ Correct:
app.count = 5; // Automatically notifiesWhy? Normal state changes already trigger notifications automatically.
Pitfall #2: Over-Notifying
❌ Wrong:
function updateMany() {
app.a = 1;
notify(app, 'a');
app.b = 2;
notify(app, 'b');
app.c = 3;
notify(app, 'c');
// Effects run 6 times total!
}✅ Correct:
function updateMany() {
batch(() => {
app.a = 1;
app.b = 2;
app.c = 3;
});
// Effects run once
}Why? Each notify() triggers effects. Batch changes instead.
Pitfall #3: Notifying Non-Reactive State
❌ Wrong:
const plain = { count: 0 };
notify(plain, 'count'); // Does nothing!✅ Correct:
const reactive = state({ count: 0 });
notify(reactive, 'count'); // Works!Why? notify() only works with reactive state.
Pitfall #4: Expecting Synchronous Execution
❌ Wrong:
let ran = false;
effect(() => {
ran = true;
});
notify(app);
console.log(ran); // false (effect queued, not run yet)✅ Correct:
let ran = false;
effect(() => {
ran = true;
});
notify(app);
await nextTick(); // Wait for effects to flush
console.log(ran); // trueWhy? notify() queues effects; they don't run immediately.
Summary
What is notify()?
notify() is a manual notification function that triggers effects for specific properties or all properties without changing state values.
Why use notify()?
- Manually trigger effects
- Integrate with external libraries
- Force UI updates
- Sync with external state
- Debug reactivity
- Control notification timing
Key Points to Remember:
1️⃣ Manual triggering - Runs effects without state changes 2️⃣ Specific or all - Notify one property or all properties 3️⃣ Queues effects - Effects run in next batch 4️⃣ External integration - Perfect for non-reactive changes 5️⃣ Use sparingly - Normal changes notify automatically
Mental Model: Think of notify() as manually ringing a doorbell - you alert everyone even though nothing changed.
Quick Reference:
// Notify specific property
notify(app, 'count');
// Notify all properties
notify(app);
// Instance method
notify(app, 'count');
notify(app);
// Common use case: external integration
function externalUpdate(newValue) {
// External system changed something
app.value = newValue;
notify(app, 'value'); // Ensure effects run
}
// Force refresh
function refresh() {
notify(app); // All effects re-run
}Remember: notify() is your manual notification tool for triggering effects. Use it for external integrations, forced updates, and debugging, but let normal state changes handle notifications automatically!