Skip to content

Sponsor

Sponsor

Understanding notify() - A Beginner's Guide

Quick Start (30 seconds)

Need to manually trigger effects without changing state? Here's how:

js
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

js
// 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):

javascript
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:

javascript
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:

javascript
// 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():

  1. Gets the metadata for the reactive state
  2. If key provided: finds effects depending on that key
  3. If no key: finds effects depending on any key
  4. Queues all found effects for execution
  5. 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():

js
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:

js
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:

js
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:

js
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:

js
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:

js
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

js
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

js
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

js
function refreshUI() {
  // Force all effects to re-run
  notify(app);
}

4. Sync with External State

js
function syncWithServer(serverData) {
  // Update local state
  Object.assign(app, serverData);

  // Notify all to ensure everything updates
  notify(app);
}

❌ Not Needed

1. Normal State Changes

js
// Don't use notify for normal updates
❌ app.count = 5;
   notify(app, 'count');  // Redundant!

// Just change the value
✅ app.count = 5;  // Automatically notifies

2. Multiple Updates

js
// 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

js
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

js
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

js
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

js
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

js
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

js
function forceUpdate(state) {
  notify(state);  // Notify all effects
}

Pattern: Selective Notification

js
function notifyMultiple(state, keys) {
  keys.forEach(key => {
    notify(state, key);
  });
}

// Usage
notifyMultiple(app, ['count', 'name', 'theme']);

Pattern: External Sync

js
function syncExternal(state, externalData) {
  batch(() => {
    Object.assign(state, externalData);
  });
  notify(state);  // Ensure everything updates
}

Pattern: Debug Trigger

js
function debugTrigger(state, key) {
  console.log(`[Debug] Manually triggering effects for "${key}"`);
  notify(state, key);
}

Common Pitfalls

Pitfall #1: Notifying After Normal Changes

Wrong:

js
app.count = 5;
notify(app, 'count');  // Redundant!

Correct:

js
app.count = 5;  // Automatically notifies

Why? Normal state changes already trigger notifications automatically.

Pitfall #2: Over-Notifying

Wrong:

js
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:

js
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:

js
const plain = { count: 0 };
notify(plain, 'count');  // Does nothing!

Correct:

js
const reactive = state({ count: 0 });
notify(reactive, 'count');  // Works!

Why? notify() only works with reactive state.

Pitfall #4: Expecting Synchronous Execution

Wrong:

js
let ran = false;

effect(() => {
  ran = true;
});

notify(app);
console.log(ran);  // false (effect queued, not run yet)

Correct:

js
let ran = false;

effect(() => {
  ran = true;
});

notify(app);
await nextTick();  // Wait for effects to flush
console.log(ran);  // true

Why? 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:

js
// 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!