Lifecycle

Run code when components mount and unmount. Set up subscriptions, fetch data, and clean up resources.

onMount

Run code once when a component first renders:

import { onMount } from 'what-framework';
import Chart from 'chart.js/auto';

function LineChart({ data }) {
  let canvas;

  onMount(() => {
    // The ref has fired and the canvas is in the DOM
    const chart = new Chart(canvas, { data });
    console.log('Chart initialized');
  });

  return <canvas ref={el => canvas = el} />;
}

Note the component is LineChart, not Chart. Naming it after the library would shadow the import, so new Chart(...) would re-enter the component instead of constructing the chart.

Use onMount for:

  • DOM measurements (element sizes, positions)
  • Setting up third-party libraries
  • Initial data fetching
  • Animations on enter

Call these in the component body

onMount and onCleanup resolve the current component from the render stack, so they must be called synchronously in the component body, never inside a callback, an effect, or after an await. In particular, calling onCleanup from inside an onMount callback throws, and the error is only logged to the console.

onMount also ignores whatever you return from it. Teardown belongs in onCleanup.

onCleanup

Register cleanup code to run when the component unmounts:

import { onMount, onCleanup } from 'what-framework';

function WebSocketChat({ roomId }) {
  let socket;

  onMount(() => {
    socket = new WebSocket(`wss://chat.example.com/${roomId}`);
    socket.onmessage = (e) => {
      console.log('Message:', e.data);
    };
  });

  onCleanup(() => {
    // Close connection when component unmounts
    socket?.close();
  });

  return <div className="chat">...</div>;
}

Use onCleanup for:

  • Removing event listeners
  • Closing connections (WebSocket, EventSource)
  • Clearing timers/intervals
  • Canceling pending requests
  • Cleaning up third-party library instances

Common Patterns

Event Listeners

function WindowSize() {
  const size = signal({ width: 0, height: 0 });

  // Declared in the body so both hooks can see it
  const updateSize = () => {
    size.set({
      width: window.innerWidth,
      height: window.innerHeight,
    });
  };

  onMount(() => {
    updateSize();
    window.addEventListener('resize', updateSize);
  });

  onCleanup(() => {
    window.removeEventListener('resize', updateSize);
  });

  return <p>{() => `${size().width} x ${size().height}`}</p>;
}

Timers

function Clock() {
  const time = signal(new Date());
  let id;

  onMount(() => {
    id = setInterval(() => {
      time.set(new Date());
    }, 1000);
  });

  // onMount discards its return value, so clear the timer here
  onCleanup(() => clearInterval(id));

  return <p>{() => time().toLocaleTimeString()}</p>;
}

Third-Party Libraries

function LeafletMap({ lat, lng }) {
  let container;
  let map;

  onMount(() => {
    map = L.map(container).setView([lat, lng], 13);
    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
  });

  onCleanup(() => {
    map?.remove();
  });

  return <div ref={el => container = el} style={{ height: '400px' }} />;
}

useEffect Alternative

For React-style lifecycle, use useEffect:

import { useEffect } from 'what-framework';

// The parent passes the signal itself: <Profile userId={userIdSignal} />
function Profile({ userId }) {
  const user = signal(null);

  // Runs on mount and again whenever userId changes
  useEffect(() => {
    fetch(`/api/users/${userId()}`)
      .then(r => r.json())
      .then(data => user.set(data));

    // Cleanup (optional)
    return () => {
      console.log('Cleanup');
    };
  }, [userId]);  // Dependencies: signal accessors only

  return <div>{() => user()?.name}</div>;
}

A deps array only tracks entries that are signal accessors. Pass the value ([userId()], or a plain prop) and the effect runs once and can never re-run, because in a run-once component a plain value never changes. What warns you in dev when it sees one. You can also drop the deps array entirely: useEffect(fn) auto-tracks every signal read inside the callback, exactly like effect.

Comparison

API When to Use
onMount Setup that runs once on mount
onCleanup Cleanup that runs on unmount
useEffect React-style effect with dependencies
effect Reactive effect (auto-tracks signals). The only entry here that does not clean itself up: pass its returned disposer to onCleanup.

Which to choose?

Use onMount/onCleanup for one-time setup. Use effect for reactive side effects that depend on signals. Use useEffect for React compatibility.

onMount and onCleanup are client-only. On a server render the component context is closed as soon as the component returns, so neither callback ever fires, and on a hydrated page they run at hydration rather than at server render.