Skip to content

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…DoDon’t
A local reactive valuesignal()useState (doesn’t exist here)
A derived / memoized valuecomputed()useMemo (doesn’t exist)
Reactive work on signal changeeffect()useEffect (doesn’t exist)
Group multiple writes into one notifybatch(fn)
Read without subscribingsignal.current() or untrack(fn)
Cleanup on disposecleanup(fn)useEffect’s return-fn pattern
Deep nested mutable statestore() from phaze/storeNesting signal() by hand
Share state without prop-drillingA signal() exported from a modulecreateContext (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:

  1. Decide the calculation is expensive. Most of the time it isn’t, and you’ve added work for nothing.
  2. Maintain a dependency array. Forget one → stale; non-reactive object dep → recomputes every render.
  3. Stabilize reference deps. Objects/arrays need their own useMemo / useRef or the comparator never hits.
  4. Eat the comparator on every render. useMemo caches the result, not the dep-comparison work, so a 1000-render parent runs the comparison 1000 times.
  5. Stack memos for downstream stability. React.memo + useCallback on the children, or the cached value still flows through full diff.
  6. 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, no useCallback.

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.

You want…DoDon’t
Mount a tree into a containerrender(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 runtimecomputed() + 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/catchtry/catch around mount
You want…DoDon’t
Run an expensive update at a lower prioritynative scheduler.postTask (or setTimeout(0) / requestIdleCallback) — Phaze doesn’t ship prioritiessee Primer › On priority and scheduling
One-shot listener cleanup on disposePass abortSignal() to listener / fetchManual removeEventListener
State-preserving keyed reorder<For> handles it (uses moveBefore)

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.

{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.

<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 JS
const 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:

componentdoes 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.

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.

theme.ts
import { signal } from '@madenowhere/phaze'
export const theme = signal<'light' | 'dark'>('light')
// any component, anywhere
import { theme } from './theme'
function Header() {
return <header class={theme}>{/* binds reactively */}</header>
}
theme.set('dark') // every binding that reads `theme` updates

No 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.