Qwik Patterns
Quick Guide: Qwik is resumable rather than hydrated — the server serializes application state into the HTML and the client picks it up without re-executing framework code. Every
$suffix marks a lazy-loading boundary the optimizer splits into its own chunk, so only the code for the interaction a user actually triggers is downloaded.component$wraps every component,useSignal/useStorehold state,routeLoader$supplies server data,routeAction$handles mutations, andserver$is ad-hoc RPC. The constraint everything else follows from: anything captured across a$boundary must be serializable.
Detailed Resources:
- examples/core.md — typed props, signals, stores, tasks, resources, events, scoped styles
- examples/routing.md — route files, nested layouts, loaders, actions,
server$, endpoints, middleware, navigation - examples/serialization.md — what crosses the
$boundary,noSerialize, lean closures, QRL props - reference.md — project layout, import cheat sheet, serializable-type table
Which path applies
The two packages divide the surface, and mixing up which one an export comes from is the commonest import error.
- Component work — components, state, lifecycle, events, slots, styles, all from
@builder.io/qwik. Follow examples/core.md. - Route and server work — routing, layouts, loaders, actions,
server$, endpoints and middleware, all from@builder.io/qwik-city. Follow examples/routing.md.
Before writing Qwik code
Wrap every component in component$(). A plain function cannot be lazy-loaded, cannot call hooks and
cannot host <Slot />.
Keep everything captured in a $ closure serializable. A non-serializable capture type-checks and
then fails at runtime, which is why the compiler is no help here.
Reach for routeLoader$ for initial server data rather than fetching in useTask$ or
useResource$. Loaders run before render and integrate with SSR streaming, so there is no loading
state to show.
Cancel default behaviour with the preventdefault:click JSX attribute rather than
event.preventDefault(). Handlers load asynchronously, so the synchronous Event APIs have already
had their effect by the time the handler runs.
Export routeLoader$ and routeAction$ from a route file — index.tsx or layout.tsx under
src/routes/. Anywhere else, or unexported, they silently do nothing.
Read and write store properties through the store reference — store.name. Destructuring extracts
the value from the Proxy, and reactivity goes with it.
Auto-detection: Qwik, component$, useSignal, useStore, useTask$, useVisibleTask$, useComputed$, useResource$, routeLoader$, routeAction$, server$, sync$, QRL, noSerialize, @builder.io/qwik, @builder.io/qwik-city, Qwik City, $(), onClick$, onInput$, Slot, q:slot, preventdefault, stoppropagation, useStylesScoped$, resumable, resumability
Applies to:
- Resumability, the
$suffix, and what the optimizer does with it - Components:
component$, typed props, QRL callback props,<Slot />projection - Reactive state:
useSignal,useStore,useComputed$ - Lifecycle:
useTask$,useVisibleTask$,useResource$ - Events:
on{Event}$,preventdefault:/stoppropagation:attributes,sync$, global listeners - Qwik City routing: file-based routes, nested and named layouts, dynamic params, route groups
- Server work:
routeLoader$,routeAction$,server$, endpoint handlers,onRequestmiddleware - Serialization rules and how to work around them
Handled elsewhere:
- Which CSS system fills a scoped style block — Qwik settles how styles attach to a component, not how they are authored.
- Validation schemas beyond the shape
zod$()wraps — the action integration is Qwik's, the schema language is not. - Databases, mail and other services called from inside
server$or a loader — those functions are just server code. - Statically-generated content sites with little interactivity — resumability buys nothing where there is nothing to resume.
Qwik is built on resumability. A hydrating framework renders HTML on the server and then re-executes every component on the client to reattach listeners and rebuild the tree. Qwik does not: the server serializes the tree, the state and the listeners into the HTML, and the client resumes from there. When a user clicks a button, that handler's chunk is what downloads — not the framework, not the tree, not the other handlers.
The $ suffix is the mechanism. Each $ is a split point the optimizer turns into a separately
loadable chunk: component$() for a render function, onClick$() for a handler, useTask$() for a
tracked effect, routeLoader$() for server-only code.
The price is serialization. A chunk that loads later needs its captured scope restored from the
HTML, so a $ closure can only close over values Qwik knows how to write down. Class instances,
functions and DOM nodes are not among them, and that single constraint explains most of the API's
unfamiliar corners — QRL props, noSerialize, the advice to keep closures lean.
Which state primitive? A single primitive is useSignal, read and written through .value. An
object or array is useStore, mutated property by property with deep tracking on by default. A value
derived synchronously from others is useComputed$. A value derived asynchronously is useResource$.
Data that comes from the server is routeLoader$.
Which lifecycle hook? useTask$ is the default; the table in Pattern 4 has the rest.
useVisibleTask$ defeats resumability, so it is for DOM measurement, browser-only APIs and canvas
work and nothing else.
Where should data loading live? Needed before the page renders: routeLoader$. Reactive to client
state: useResource$, calling server$ when the work belongs on the server. Triggered by the user:
routeAction$ for anything form-shaped, server$ called from a handler when there is no form.
Does the handler need a synchronous Event API? preventDefault becomes the
preventdefault:eventname attribute, stopPropagation becomes stoppropagation:eventname, and
currentTarget becomes the handler's second parameter. Everything else is an ordinary on{Event}$,
extracted with $() and typed as QRL when it is reused.
Which styling attachment? useStylesScoped$ scopes styles to the component and lazy-loads them with
it, with :global() as the escape hatch for projected <Slot /> content. useStyles$ attaches
unscoped styles the same way. Anything site-wide is imported in the root layout. Runtime CSS-in-JS that
injects styles during render is incompatible with SSR streaming here; zero-runtime approaches are not.
Core patterns
Pattern 1: Components with component$
Every component is wrapped, and the wrapper is what gives it lazy loading, hooks and <Slot />.
export const Counter = component$<{ initial?: number; label: string }>(
({ initial = 0, label }) => {
const count = useSignal(initial);
return (
<button onClick$={() => count.value++}>
{label}: {count.value}
</button>
);
},
);
Full code: examples/core.md
Pattern 2: useSignal and useStore
useSignal holds one value behind .value. useStore holds an object and tracks it deeply, mutated in
place — but only through the store reference.
const isEditing = useSignal(false);
const user = useStore({ name: "Alice", preferences: { theme: "dark" } });
user.name = "Bob"; // reactive
const { name } = user; // plain string — reactivity lost
Full code: examples/core.md
Pattern 3: Derived values with useComputed$
Synchronous derivation with automatic dependency tracking and no dependency array. The result is a read-only signal.
const subtotal = useSignal(85);
const tax = useComputed$(() => subtotal.value * TAX_RATE);
const total = useComputed$(() => subtotal.value + tax.value);
Full code: examples/core.md
Pattern 4: Tasks and lifecycle
track() declares what re-runs the task; cleanup() runs before each re-run and on teardown.
useTask$(({ track, cleanup }) => {
const term = track(() => query.value);
const timer = setTimeout(() => search(term), DEBOUNCE_MS);
cleanup(() => clearTimeout(timer));
});
| Hook | Runs | Use for |
|---|---|---|
useTask$ |
Server + client, before render | Data init, side effects on state change |
useVisibleTask$ |
Browser only, after render | DOM manipulation, browser APIs, animations |
useComputed$ |
Synchronous, auto-tracked | Derived values (formatting, filtering) |
useResource$ |
Server + client, non-blocking | Async data that should not block render |
A useTask$ that tracks nothing runs once, as an initialization hook rather than a reactive effect.
Full code: examples/core.md
Pattern 5: Event handling
Handlers are on{Event}$ and load asynchronously, so default-behaviour control is declarative and the
element arrives as the second parameter instead of through currentTarget.
<form preventdefault:submit onSubmit$={handleSubmit}>
<input value={email.value} onInput$={(_, el) => (email.value = el.value)} />
</form>
Full code: examples/core.md
Pattern 6: Content projection with <Slot />
<Slot /> takes children; q:slot on a direct child of the usage site routes into a named slot. Both
work only inside component$().
<div class="card">
<header>
<Slot name="header" />
</header>
<div class="body">
<Slot />
</div>
</div>
Wrapping slotted content in an intermediate element breaks the projection — q:slot must sit on a
direct child.
Full code: examples/core.md
Pattern 7: routeLoader$ for server data
Exported from a route file, runs server-side before render, and returns a read-only signal. fail()
returns a typed error rather than throwing.
export const useProduct = routeLoader$(async (requestEvent) => {
const product = await db.products.findById(requestEvent.params.id);
return product ?? requestEvent.fail(404, { errorMessage: "Not found" });
});
export default component$(() => {
const product = useProduct();
return <h1>{product.value.name}</h1>;
});
Full code: examples/routing.md
Pattern 8: routeAction$ for mutations
Handles form submissions server-side, with zod$() for validation and per-field errors. <Form> works
without JavaScript, so the page degrades to a plain POST.
export const useContactAction = routeAction$(
async (data) => {
await sendEmail(data);
return { success: true };
},
zod$({ email: z.string().email(), message: z.string().min(10) }),
);
<Form action={action}>
{action.value?.fieldErrors?.email && <p>{action.value.fieldErrors.email}</p>}
<button type="submit" disabled={action.isRunning}>
Send
</button>
</Form>
Full code: examples/routing.md
Red flags
Breaks at runtime:
- A component written as a plain function — hooks throw,
<Slot />silently fails, and the optimizer cannot split it. - A class instance, function or DOM node captured in a
$closure — type-checks, then throws a serialization error. - A destructured store —
const { name } = storeyields a plain value, and every later write is invisible to the UI. event.preventDefault()orevent.stopPropagation()inside a handler — a no-op, because the handler ran too late. Use thepreventdefault:/stoppropagation:attributes.event.currentTargetinside a handler — null in an async handler; take the element from the second parameter.routeLoader$orrouteAction$outsidesrc/routes/**/index.tsxorlayout.tsx, or defined but not exported — no error, and no execution either.- A plain function type on a callback prop — callback props are
QRL<() => void>and their values are wrapped in$(). - An import taken from the wrong package — components, signals and tasks are
@builder.io/qwik; routing, loaders, actions andserver$are@builder.io/qwik-city.
Surprising behaviour:
useVisibleTask$whereuseTask$would do gives up SSR and resumability for that component.useTask$blocks the render until it settles, so long async work belongs inuseResource$.- Fetching in
useTask$instead ofrouteLoader$costs SSR streaming and adds a loading state the user should never have seen. - Closing over a whole store to reach one property makes Qwik serialize the whole store.
- Islands-style thinking does not apply — every component is already lazy at the interaction level, so there is nothing to choose to hydrate.
useStore({ deep: true })is the default written out;{ deep: false }is the option that changes anything.- An arrow function as a store method loses
this— usefunction () {}. - An inline
<style>tag in a component double-loads, once from SSR and once from the client. useStylesScoped$scopes through emoji-based class selectors, which some CSS parsers and test tools mishandle;:global()is how you reach<Slot />content through it.- Middleware in
layout.tsxdoes not run forserver$calls — put checks aserver$call must pass inplugin.tsor inside the function. server$needs client and server on the same deployed version; a stale client calling a moved function is undefined behaviour.- Props are shallowly immutable, so a child reassigning a primitive prop does nothing — pass a
Signalwhen the child must write back. - Deep store tracking follows the property you read, so tracking
store[key]does not trackstore[key].nested.