Decisions
Phaze has fewer named primitives than React because every reactive boundary is explicit and most live in plain JS. The trade-off: you can read a Phaze component and know exactly what re-runs when. This page is the one-screen lookup for “what do I use here.”
| You want… | Do | Don’t |
|---|---|---|
| A local reactive value | signal() | useState (doesn’t exist here) |
| A derived / memoized value | computed() | useMemo (doesn’t exist) |
| Reactive work on signal change | effect() | useEffect (doesn’t exist) |
| Group multiple writes into one notify | batch(fn) | — |
| Read without subscribing | signal.current() or untrack(fn) | — |
| Cleanup on dispose | cleanup(fn) | useEffect’s return-fn pattern |
| Deep nested mutable state | store() from phaze/store | Nesting signal() by hand |
| Share state without prop-drilling | A signal() exported from a module | createContext (Phaze doesn’t ship one) |
Phaze’s computed() vs React/Preact useMemo()
Section titled “Phaze’s computed() vs React/Preact useMemo()”useMemo is a hint, not a guarantee. To use it correctly the author has to:
- Decide the calculation is expensive. Most of the time it isn’t, and you’ve added work for nothing.
- Maintain a dependency array. Forget one → stale; non-reactive object dep → recomputes every render.
- Stabilize reference deps. Objects/arrays need their own
useMemo/useRefor the comparator never hits. - Eat the comparator on every render.
useMemocaches the result, not the dep-comparison work, so a 1000-render parent runs the comparison 1000 times. - Stack memos for downstream stability.
React.memo+useCallbackon the children, or the cached value still flows through full diff. - Trust a cache React 19 doesn’t promise to keep — which is why React Compiler exists.
computed() doesn’t ask any of those questions:
- No “is this expensive?” decision. Setup is constant, recompute fires only when a tracked input actually changes. A trivial computed costs nothing; an expensive one runs only when its data flips.
- No dependency array. Reads auto-subscribe; the framework knows the exact set. You can’t forget a dep.
- No reference dance. A
computed()returns the same reactive handle every read; consumers subscribe to it. - Components don’t re-run. No render cadence pumps work through the memo. The computed sits in the reactive graph and pulls when its inputs notify it.
- Downstream propagation is automatic. Bindings reading the computed re-run only when the computed actually changed. No
React.memo, nouseCallback.
The mental flip: in React you opt into performance by deciding what’s expensive, listing deps, stabilizing references, and stacking memos. In Phaze, performance is structural. The runtime already knows what depends on what — there is nothing for you to forget.
Rendering
Section titled “Rendering”| You want… | Do | Don’t |
|---|---|---|
| Mount a tree into a container | render(component, container) | — |
| Pick one of two branches | {cond() ? <A/> : <B/>} | nothing — same JSX as React |
| Pick one of three+ branches (independent show/hide) | three lines of {cond() && <X/>} | nested ternaries |
| Pick one of many branches (mutually exclusive) | computed() + if-chain | <Switch> (doesn’t ship) |
| Render a keyed list | <For each={() => items} getKey={...}> | items.map(...) (no reorder identity) |
| Render a component chosen at runtime | computed() + if-chain | <Dynamic> (doesn’t ship) |
| Render somewhere else in the DOM | <Portal mount={el}> from phaze/portal | — |
| Catch errors gracefully | <Catch fallback={...}> from phaze/catch | try/catch around mount |
Scheduling & teardown
Section titled “Scheduling & teardown”| You want… | Do | Don’t |
|---|---|---|
| Run an expensive update at a lower priority | native scheduler.postTask (or setTimeout(0) / requestIdleCallback) — Phaze doesn’t ship priorities | see Primer › On priority and scheduling |
| One-shot listener cleanup on dispose | Pass abortSignal() to listener / fetch | Manual removeEventListener |
| State-preserving keyed reorder | <For> handles it (uses moveBefore) | — |
The pattern
Section titled “The pattern”Across all of these, one rule: reactive props are functions.
<For each={() => todos()} getKey={t => t.id}>...</For>A function inside the prop means “re-evaluate when its signals change”; a plain value means “fix at construction.” This is the one convention that ties the API together.
Three concrete recipes
Section titled “Three concrete recipes”Two-state UI: ?:
Section titled “Two-state UI: ?:”{loggedIn() ? <Dashboard/> : <LoginButton/>}Native JS. Same shape as React. Reactive.
Independent show/hide (e.g. tabs): three && lines
Section titled “Independent show/hide (e.g. tabs): three && lines”return ( <div> {active() === 'overview' && <Overview {...sharedProps}/>} {active() === 'details' && <Details {...sharedProps}/>} {active() === 'settings' && <Settings {...sharedProps}/>} </div>)Each line owns one tab. Adding/removing tabs is a one-line edit.
Open-ended multi-branch: computed() + if-chain
Section titled “Open-ended multi-branch: computed() + if-chain”const view = computed(() => { if (route() === '/home') return <HomePage/> if (route() === '/profile') return <Profile user={user()}/> if (!user()) return <Welcome/> return <NotFound/>})
return <main>{view}</main>Native control flow inside a phaze computed (memoization). No JSX wrapper component, no nesting, default is the last return.
Why no <Dynamic>
Section titled “Why no <Dynamic>”<Dynamic> exists in React/Solid because their reconcilers need a value-shaped slot to dispatch component swaps through. Phaze has no reconciler — JSX is just function calls; passing a component reference to jsx() works without ceremony, and computed() is the natural reactive boundary for “which component renders here.”
// React/Preact's reconciler reason<Dynamic component={() => isAdmin() ? AdminPanel : UserPanel} {...props}/>
// Phaze: just JSconst view = computed(() => { const C = isAdmin() ? AdminPanel : UserPanel return <C {...props}/>})
return <main>{view}</main>The Phaze form is one identifier longer at the use site, but it doesn’t introduce a new flow component — and it scales naturally to more branches via additional if lines, with no per-branch prop repetition.
The remaining named JSX components in Phaze each do something computed + if can’t:
| component | does what JS can’t |
|---|---|
<For> (core) | LIS-based keyed reconciliation + moveBefore for state-preserving reorders |
<Catch> (subpath phaze/catch) | hooks the runtime’s computation-tree error walk |
<Portal> (subpath phaze/portal) | mounts DOM at a different target (out of the local tree) |
Three components. One in core, two opt-in. Each earns its keep.
Why no createContext
Section titled “Why no createContext”Phaze does not ship a context API. In a signals-native runtime, a module-level signal() already does what context exists for: a value any component can subscribe to, reactively, without prop-drilling. The import graph scopes it; the signal IS the subscription.
import { signal } from '@madenowhere/phaze'export const theme = signal<'light' | 'dark'>('light')
// any component, anywhereimport { theme } from './theme'
function Header() { return <header class={theme}>{/* binds reactively */}</header>}
theme.set('dark') // every binding that reads `theme` updatesNo Provider tree. No useContext. No render-prop. No “captured at construction” footnote. This is the same answer for theme, auth, locale, router state, app config, and the long tail of state most React apps use context to share.
For the rare case where you need tree-scoped overrides (e.g. nested theme sections on the same page), pass the signal as a prop or define multiple signals. There is no createContext in Phaze, and there will not be one. The signals-first answer is the only answer.