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
| 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 the compiler wraps it.
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) |
Never Call .set() in the Component Body
In React, calling setState during render is bad practice but React catches it and shows a warning. In What, it's an instant infinite loop:
// INFINITE LOOP — don't do this
function Broken() {
const count = useSignal(0);
count.set(c => c + 1); // Signal updates → component re-runs → set again → forever
return <p>{count()}</p>;
}
Signal writes always go 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(() => { return cleanup }) |
useEffect(fn, [dep]) |
effect(fn) (auto-tracks) |
| Cleanup on unmount | onCleanup(fn) |
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()
What has two ways to create signals:
| API | Where to use | Cleanup |
|---|---|---|
signal(value) |
Anywhere — module scope, outside components, stores | Manual |
useSignal(value) |
Inside components only | Automatic (tied to component lifecycle) |
// Module-level signal — shared between all instances, lives forever
const theme = signal('dark');
function Counter() {
// Component-level signal — each Counter gets its own, cleaned up on unmount
const count = useSignal(0);
return <button onClick={() => count.set(c => c + 1)}>{count()}</button>;
}
Rule of thumb: use useSignal in components (most of the time), signal for global/shared state.
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 |
Same pattern |
useContext |
useContext |
Same API |
Suspense |
Suspense |
Same pattern |
lazy() |
lazy() |
Same API |
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. Calling .set() in the component body
// Infinite loop in What
function Bad() {
const x = useSignal(0);
x.set(1); // Triggers re-render → set → re-render → forever
return <p>{x()}</p>;
}
// Use onMount for initialization
function Good() {
const x = useSignal(0);
onMount(() => x.set(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 lets you use React libraries (zustand, TanStack Query, Radix UI, and 90+ more) directly in What components:
import { mount, useSignal } 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);
return <button onClick={increment}>{count}</button>;
}
mount(<App />, '#app');
See the React Compat docs for setup instructions.