Effects

Run side effects in response to signal changes. Effects handle everything that isn't just returning UI.

What Are Effects?

Effects are functions that run automatically when their dependencies change. They're for side effects: things like:

  • Fetching data
  • Setting up subscriptions
  • Updating the document title
  • Logging
  • Syncing with external systems
import { signal, effect } from 'what-framework';

const count = signal(0);

// Runs immediately, then re-runs when count changes
effect(() => {
  console.log('Count is now:', count());
});

count.set(5);  // Logs: "Count is now: 5"

How Effects Work

When you create an effect:

  1. The function runs immediately
  2. What tracks which signals are read during execution
  3. When any of those signals change, the effect re-runs
const a = signal(1);
const b = signal(2);

effect(() => {
  console.log(a() + b());  // Tracks both a and b
});

a.set(10);  // Effect re-runs, logs 12
b.set(20);  // Effect re-runs, logs 30

No dependency arrays

Unlike React's useEffect(fn, [deps]), What effects auto-track. Every signal read inside the function becomes a dependency automatically. You never need to maintain a deps array, and you'll never have stale closure bugs.

If you only need code to run once on mount (like React's useEffect(fn, [])), use onMount instead.

Cleanup Functions

Effects can return a cleanup function that runs before the next execution or when the effect is disposed:

effect(() => {
  const handler = () => console.log('resize');
  window.addEventListener('resize', handler);

  // Cleanup: remove the listener
  return () => {
    window.removeEventListener('resize', handler);
  };
});

Common uses for cleanup:

  • Removing event listeners
  • Clearing timers/intervals
  • Canceling network requests
  • Closing connections

Practical Examples

Document Title

const unreadCount = signal(0);

effect(() => {
  const count = unreadCount();
  document.title = count > 0
    ? `(${count}) My App`
    : 'My App';
});

Fetch Data

const userId = signal(1);
const user = signal(null);
const loading = signal(false);

effect(() => {
  const id = userId();
  loading.set(true);

  fetch(`/api/users/${id}`)
    .then(r => r.json())
    .then(data => {
      user.set(data);
      loading.set(false);
    });
});

Local Storage Sync

const theme = signal(
  localStorage.getItem('theme') || 'light'
);

effect(() => {
  localStorage.setItem('theme', theme());
});

Debounced Search

const query = signal('');
const results = signal([]);

effect(() => {
  const q = query();
  if (!q) {
    results.set([]);
    return;
  }

  const timeout = setTimeout(() => {
    fetch(`/api/search?q=${q}`)
      .then(r => r.json())
      .then(data => results.set(data));
  }, 300);

  return () => clearTimeout(timeout);
});

Effects vs Computed

Use computed for derived values (pure transformations):

// GOOD - derived value
const fullName = computed(() => `${first()} ${last()}`);

Use effect for side effects (interactions with the outside world):

// GOOD - side effect
effect(() => {
  document.title = fullName();
});

Avoid: Effects that just set signals

// BAD - use computed instead
const doubled = signal(0);
effect(() => {
  doubled.set(count() * 2);
});

// GOOD
const doubled = computed(() => count() * 2);

Conditional Tracking

Only signals read during execution are tracked:

const showDetails = signal(false);
const details = signal('...');

effect(() => {
  if (showDetails()) {
    console.log(details());  // Only tracks details when showDetails is true
  }
});

details.set('new');  // Doesn't trigger (showDetails is false)
showDetails.set(true);  // Triggers, now details is tracked
details.set('newer');  // Triggers

Reading Without Tracking

Use peek() or untrack() to read without creating a subscription:

import { untrack } from 'what-framework';

const count = signal(0);
const multiplier = signal(2);

effect(() => {
  // Re-runs when count changes, NOT when multiplier changes
  const result = count() * multiplier.peek();
  console.log(result);
});

// Alternative syntax
effect(() => {
  const result = count() * untrack(() => multiplier());
  console.log(result);
});

Best Practices

  • Keep effects focused. One effect, one purpose.
  • Always clean up. If you add a listener, remove it in cleanup.
  • Prefer computed for derived values. Effects are for side effects only.
  • Don't modify signals you're reading. This can cause infinite loops.
  • Never call .set() in the component body. Use event handlers, effects, or onMount.
  • Use onMount for one-time setup. Don't use effect when you just need code to run once.
  • Handle errors. Wrap async code in try/catch.