Coming from React

If you know React, you already know most of What. This page covers the key differences so you can be productive immediately.

The Big Difference: No Re-Renders

In React, when state changes, the entire component function re-runs. Every variable is recalculated, every expression is re-evaluated, and React diffs a tree of objects to figure out what changed in the DOM.

In What, component functions run once. Signals update the specific DOM nodes that depend on them: no intermediate representation, no diffing, no re-running the whole function. The compiler transforms JSX into direct DOM operations at build time.

This is faster, but it means some React patterns don't translate directly.

How JSX Compiles

In React, JSX compiles to React.createElement() calls that build an object tree, which React diffs against the previous tree. In What, the compiler extracts static HTML into cloneable templates and wraps dynamic expressions in fine-grained effects:

JSX -> compiler -> template() + insert() + effect() -> DOM

Static parts are never recreated. Only the specific text nodes or attributes that depend on a signal are updated when that signal changes.

State: useState vs useSignal

To ease the transition, What ships useSignal, a thin shim that mirrors React's use* naming. It's used throughout this page so the React↔What mapping stays one-to-one. The canonical API is plain signal(), which works everywhere and returns the same object; reach for it once the hook habit fades.

React What
const [x, setX] = useState(0) const x = useSignal(0)
x (read) x() (read, call it like a function)
setX(5) x.set(5)
setX(prev => prev + 1) x.set(prev => prev + 1)
// React
function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

// What
function Counter() {
  const count = useSignal(0);
  return <button onClick={() => count.set(c => c + 1)}>{count()}</button>;
}

Derived Values: Why useComputed Exists

This is the #1 thing that trips up React developers.

In React, const doubled = count * 2 just works because the whole function re-runs on every state change: doubled gets recalculated every time.

In What, the component function runs once. So const doubled = count() * 2 evaluates to a plain number at creation time and never updates:

// BROKEN: doubled is a dead number, computed once, never updates
function App() {
  const count = useSignal(0);
  const doubled = count() * 2;  // Evaluates to 0, stays 0 forever

  return <p>Doubled: {doubled}</p>;  // Always shows "Doubled: 0"
}

Use useComputed to create a derived signal that tracks its dependencies and updates automatically:

// CORRECT: useComputed tracks count and re-derives when it changes
function App() {
  const count = useSignal(0);
  const doubled = useComputed(() => count() * 2);

  return <p>Doubled: {doubled()}</p>;  // Updates when count changes
}

When do you need useComputed?

If you're computing a value from signals and need to use it in multiple places or pass it to child components, use useComputed. It caches the result and only recomputes when dependencies change.

For simple expressions used only once in JSX, the compiler handles reactivity automatically: {count() * 2} works directly in JSX because what-compiler wraps it in an effect. That is the default setup create-what scaffolds, and it is what every sample on this page assumes. Without the compiler (the buildless full-stack template, which uses h() rather than JSX) the same expression is evaluated once and never updates, so write it as a thunk instead: h('p', {}, () => count() * 2).

The rule

React pattern What equivalent
const x = a + b const x = useComputed(() => a() + b())
useMemo(() => expensive(), [dep]) useComputed(() => expensive(dep()))
{count * 2} in JSX {count() * 2} in JSX (compiler handles it)

Writing a Signal in the Component Body

In React, calling setState during render is bad practice but React catches it and shows a warning. What has no render pass to re-enter: the component body runs exactly once, inside untrack(), so a write there cannot re-trigger the component. It is not an error and it is not a loop, it is just a slower way of writing the initial value:

// REDUNDANT, not broken. The body runs once, so this renders 1
function Redundant() {
  const count = useSignal(0);
  count.set(c => c + 1);  // Runs once. No re-render, no loop.
  return <p>{count()}</p>;
}

Give the signal the value you actually want (useSignal(1)) and keep the body free of writes, so the component reads as a description of its starting state. Signal writes that depend on something belong in one of these places:

// 1. Event handlers
<button onClick={() => count.set(c => c + 1)}>+1</button>

// 2. Effects (for reactive side effects)
effect(() => {
  if (query() && query().length > 2) {
    fetchResults(query());
  }
});

// 3. onMount (for one-time initialization)
onMount(() => {
  count.set(parseInt(localStorage.getItem('count')) || 0);
});

Effects: Auto-Tracking vs Dependency Arrays

React effects require you to list dependencies manually:

// React: you manage the deps array
useEffect(() => {
  document.title = `Count: ${count}`;
}, [count]);  // Must list count or it won't re-run

What effects auto-track: any signal read inside the effect becomes a dependency automatically:

// What: no deps array needed
effect(() => {
  document.title = `Count: ${count()}`;
  // Automatically re-runs when count changes
  // No dependency array, tracking is automatic
});

What about useEffect?

What provides useEffect(fn, deps) for React compatibility, but the idiomatic approach is effect(fn) with auto-tracking. You'll never have a stale closure or missing dependency bug again.

Opting out of tracking

Sometimes you want to read a signal without subscribing to it. Use peek() or untrack():

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

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

peek() is shorthand for reading one signal. untrack() wraps a block of code where nothing inside creates subscriptions.

Lifecycle: onMount vs useEffect(fn, [])

In React, "run once on mount" is useEffect(() => {}, []). In What, use onMount:

React What
useEffect(fn, []) onMount(fn)
useEffect(() => { return cleanup }, []) onMount(fn) plus onCleanup(cleanup)
useEffect(fn, [dep]) effect(fn) (auto-tracks)
Cleanup on unmount onCleanup(fn)

One difference worth memorising: unlike React's useEffect, onMount ignores a function you return from it. The return value is dropped, so a React teardown ported one-to-one leaks its listener or timer silently. Register teardown with onCleanup instead, as below.

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

function Chat({ roomId }) {
  let socket;

  onMount(() => {
    socket = new WebSocket(`wss://chat.example.com/${roomId}`);
  });

  onCleanup(() => {
    socket?.close();
  });

  return <div>Connected to {roomId}</div>;
}

signal() vs useSignal()

signal() is the canonical API. Use it everywhere: module scope, inside components, and in stores. Because What components run once, a signal() declared in a component body executes exactly once (not per-render like a React hook), so there is no hook-ordering rule and no need for a separate component-only primitive.

API Where to use Notes
signal(value) Anywhere: module scope, components, stores Canonical. Read with count(), write with count(v) (or the explicit count.set(v))
useSignal(value) Inside components (optional) A thin compat shim for React muscle memory. Returns the same signal object as signal(); identical behavior in the run-once model
// Module-level signal: shared between all components
const theme = signal('dark');

function Counter() {
  // Component-level signal: runs once, local to this component
  const count = signal(0);

  return <button onClick={() => count(c => c + 1)}>{count()}</button>;
}

Rule of thumb: reach for signal() everywhere. useSignal() exists if you prefer the React use* naming inside components, and it returns the same thing.

Quick Reference

React What Notes
useState useSignal Read with (), write with .set()
useMemo useComputed Auto-tracks, no deps array
useEffect(fn, [deps]) effect(fn) Auto-tracks signals, no deps array
useEffect(fn, []) onMount(fn) Runs once after first render
useRef useRef Same API
useCallback Not needed No re-renders means no stale closures
React.memo Not needed Components don't re-render by default
createContext createContext Pass the signal to Provider, not its current value
useContext useContext Call it in the component body, never in a handler
Suspense Suspense Same pattern
lazy() lazy() Same API

Context: two rules the run-once model adds

The names match React, but the run-once model changes how you feed and read a context. A Provider body also runs once, so a value computed from a signal is captured at that moment and never refreshed. Pass the signal itself and let the consumer call it. And read the context during the component body: useContext called inside an event handler has no component to walk up from, so it returns the context's default value and logs a warning.

// BROKEN: value is read once, consumers never see a change
<Theme.Provider value={theme()}><Panel /></Theme.Provider>;

// CORRECT: hand over the signal itself
<Theme.Provider value={theme}><Panel /></Theme.Provider>;

function Panel() {
  const theme = useContext(Theme);  // read in the body
  const onClick = () => console.log(theme());  // close over it

  return <div onClick={onClick}>{() => theme()}</div>;
}

Common Mistakes from React Habits

1. Forgetting to call the signal

// React habit: count is already a value
<p>{count}</p>

// What: count is a function, call it
<p>{count()}</p>

2. Inline math without useComputed

// React habit: works because component re-runs
const doubled = count * 2;

// What: need useComputed for derived values
const doubled = useComputed(() => count() * 2);

3. Writing a signal in the component body

// Harmless but pointless. The body runs once, so this is a slow useSignal(1)
function Bad() {
  const x = useSignal(0);
  x.set(1);
  return <p>{x()}</p>;
}

// Start the signal at the value you want
function Good() {
  const x = useSignal(1);
  return <p>{x()}</p>;
}

4. Adding unnecessary dependency arrays

// React habit: manually listing deps
effect(() => {
  document.title = count();
}, [count]);  // ← Not needed, and doesn't do what you think

// What: just use the signal, tracking is automatic
effect(() => {
  document.title = count();
});

Using React Libraries in What

You don't have to choose between What and the React ecosystem. The what-react compat layer runs real React libraries on What's engine. The libraries verified end to end (headless browser plus CI) are zustand, TanStack Query, react-hook-form, react-hot-toast, Headless UI, Framer Motion and Recharts. Anything beyond those seven is plausible but untested on the current runtime, so treat it as unverified until you have tried it.

Inside a compat component, React semantics apply, not What's

Compat works by aliasing every react and react-dom import to what-react and pointing JSX at the React runtime, for the whole project. Inside a compat component the rules above do not apply: hooks return values rather than accessors, and a bare {count()} renders once and never updates. Compat components are also client-only and cannot be server-rendered.

// vite.config.js: reactCompat() sets up the aliases and the JSX runtime
import { defineConfig } from 'vite';
import { reactCompat } from 'what-react/vite';

export default defineConfig({
  plugins: [reactCompat()],
});

With that in place, the component is ordinary React code and the library works untouched:

import { mount } from 'what-framework';
import { create } from 'zustand';

const useStore = create((set) => ({
  count: 0,
  increment: () => set((s) => ({ count: s.count + 1 })),
}));

function App() {
  const count = useStore((s) => s.count);
  const increment = useStore((s) => s.increment);

  // React semantics: count is a number, not an accessor
  return <button onClick={increment}>{count}</button>;
}

mount(<App />, '#app');

The fastest way to get a working project is create-what with React support enabled, which wires the plugin and ships this zustand demo. See the React Compat docs for the full verified matrix and the known limitations: no server rendering, a minimal Suspense that unmounts suspended subtrees (losing their state), and errors thrown inside effects being logged rather than routed to an error boundary.