Diagnostics
Stewie emits diagnostics with stable STW### codes to catch common mistakes early. Compiler diagnostics surface at build time (and in your editor via the Vite plugin); a few dev-runtime warnings catch things that only show up when code runs. Each message links back to this page.
The compiler is optional — projects using plain JSX without @stewie-js/vite still get the dev-runtime warnings, but not the build-time ones. Where a rule can run in both places it does.
TIP
These are guidance, not gates. Most are warnings. They point at a footgun and suggest the fix; the entry here explains why.
Module-scope reactivity
STW001 — signal() at module scope
Creating a signal() (or computed/store/effect) at module scope makes it an accidental singleton shared across every SSR request. Move the call inside a component body or reactiveScope().
STW002 — computed() at module scope
Same as STW001: a module-scope computed() is shared across requests. Create it inside a component or reactiveScope().
STW003 — store() at module scope
A module-scope store() leaks state across SSR requests. Create it inside a component or reactiveScope().
STW004 — effect() at module scope
Effects must be owned by a component or reactiveScope() so they can be disposed. A module-scope effect is never cleaned up.
STW005 — useAction() outside a component or reactiveScope()
useAction() creates per-call-site pending/error signals that need an owning scope. Call it inside a component body or reactiveScope(). (defineAction() at module scope is fine — it creates no signals.)
STW006 — useResource() outside a component or reactiveScope()
useResource() creates per-call-site data/loading/error signals that need an owning scope. Call it inside a component or reactiveScope(). (defineResource() at module scope is fine.)
STW007 — useTitle() / useMeta() outside a component or reactiveScope()
These create reactive effects that write document.head and must be disposed on unmount. Call them inside a component or reactiveScope().
Signals in JSX
STW010 — Signal referenced but not called in a JSX child
{count} renders the function itself, not its value. Call it: {count()}, or wrap as a function child {() => count()} for a reactive slot.
STW011 — Signal referenced but not called in a JSX attribute
attr={sig} sets the attribute to the function. Call it: attr={sig()} (static read) or attr={() => sig()} (reactive).
STW014 — peek() inside a reactive context
peek() reads without subscribing, so an effect()/computed() that only peek()s a signal won't re-run when it changes. Call the signal directly (sig()) for reactivity, or move the peek() outside the reactive body if the non-tracking read is intentional.
Control flow
STW020 — <Show when> given an eager signal read
when={isOpen()} reads the signal once at mount, so the condition never re-evaluates. Pass the signal directly (when={isOpen}) or wrap it (when={() => isOpen()}). Only genuine Signal/Computed reads are flagged — a static helper call isn't.
STW021 — <For each> given an eager signal read
each={tasks()} reads the signal once at mount, so the list never reacts to changes. Pass the signal directly (each={tasks}) or wrap it (each={() => tasks()}).
STW022 — <For by> returns a non-unique key
A by key function returning a constant or the identity of its parameter breaks keyed reconciliation — rows collapse or re-render incorrectly. Return a per-item unique id: by={(item) => item.id}.
Reactive scope and lifecycle
STW040 — signal() created inside an effect() body
The signal is re-created on every effect run, so its state resets each time and nothing outside the effect can read it. Hoist the signal() call above the effect().
STW041 — onCleanup() called outside a reactive scope
With no owning scope, the cleanup callback is dropped and will never run. Call onCleanup() inside a component body or reactiveScope() so it's tied to a lifecycle. Dev-runtime warning.
STW042 — effect() created inside a computed() body
Computeds must be pure. An effect created here is never cleaned up and can loop when the computed re-evaluates. Move the effect to a component body or reactiveScope().
STW043 — Writing to a signal inside a computed() body
computed(() => { count.set(1); return ... }) mutates state during a pure computation — it can loop or produce surprising results. Move the write to an event handler, effect(), or an action. Type-aware (only genuine signal writes are flagged, not e.g. map.set()).
Context
STW050 — consume() with no ancestor provide()
The context has no provider above the consumer and no default value, so there's nothing to read. Wrap the consumer in a provider, or give the context a default: createContext(defaultValue). Dev-runtime (throws).
STW052 — createContext() called outside module scope
Each createContext() call creates a new context identity, so provide()/consume() pairs across renders won't match. Move createContext() to module top level.
Resource
STW063 — defineResource() without a stable id used with SSR replay
Without an explicit { id }, an auto-counter id is assigned that is not stable across the separate SSR and client builds. To stay safe, an unkeyed resource does not participate in the DataRegistry at all — its SSR-resolved data is refetched on the client and it won't dedupe across components. Pass an explicit id for SSR replay: defineResource(fn, { id: 'fetchUser' }).
Router
STW073 — <Link to> is an external URL
<Link> is for internal client-side navigation. An http(s):// target should be a plain <a href rel="noopener noreferrer">.
STW076 — useParams() read a key the matched route does not declare
The generic on useParams<{ slug: string }>() is a phantom-type carrier — it is never checked against the route's path. Reading a key the matched route does not declare returns undefined under a type that says it is present, and that undefined then flows into app code as if it were a string.
Prefer the value form, where the param names come from the path literal and a wrong key is a compile error:
const ProductRoute = createRoute('/products/:productId', { component: ProductPage })
const { productId } = useParams(ProductRoute) // inferred from the pathWarns on property reads only — 'key' in params, Object.keys(params), and spreading stay silent. Dev-only.
SSR / hydration
STW083 — window / document accessed at module scope
The module throws on import in SSR / non-browser environments. Move the access inside a component or effect(), or guard with typeof window !== 'undefined'.
STW100 — mount() called on the server
mount() drives real DOM and is client-only. In a non-browser environment (no document) it fails. Use renderToString() / renderToStream() from @stewie-js/server for SSR. Dev-runtime warning.
Two-way binding ($prop)
STW090 — $prop target is not a signal
$value={x} compiles to x.set(...), so the target must be a writable signal(). A plain value has no .set() and fails at runtime. Pass a signal, or use a one-way binding (value={x}) for a static value.
STW091 — $prop target is read-only
A computed() or a plain () => T accessor is callable but has no .set(), so it can't be a two-way binding target. Use a writable signal() for $prop, or read it one-way (value={x()}).
STW092 — Both $prop and prop specified
$value already implies value. Having both is contradictory — remove the plain value attribute.
STW093 — $prop is not a recognized two-way binding
Only $value (input/textarea/select) and $checked (checkbox/radio) are recognized — they map to input/change events. Any other $prop has no event to write back through, so the binding is silently dead. For custom two-way flow, read the signal and write it back in an explicit event handler.
STW094 — $prop on a readonly element
A two-way binding on a readonly element can't write back, so it's downgraded to one-way. Drop readonly, or use a plain one-way value={...}.
STW095 — $prop on a disabled element
Same as STW094 for disabled elements — the binding is downgraded to one-way.
