Qwik Framework Patterns
Quick Guide: Qwik is resumable - it serializes application state on the server and resumes on the client without re-executing framework code (no hydration). Every
$suffix marks a lazy-loading boundary where the optimizer splits code into separate chunks. Only the code for the interaction the user triggers gets downloaded. Usecomponent$for all components,useSignal/useStorefor state,routeLoader$for server data,routeAction$for mutations, andserver$for ad-hoc server RPC. The critical mental model: anything crossing a$boundary must be serializable.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST wrap every component in component$() - plain functions cannot be lazy-loaded, cannot use hooks, and cannot use <Slot />)
(You MUST ensure all values captured in a $ closure are serializable - non-serializable captures pass type-checking but fail at runtime)
(You MUST use routeLoader$ for initial server data instead of fetching in useTask$ or useResource$ - loaders run before render and integrate with SSR streaming)
(You MUST use preventdefault:click as a JSX attribute instead of calling event.preventDefault() - event handlers load asynchronously so synchronous Event APIs are unavailable)
(You MUST export routeLoader$ and routeAction$ from route files (index.tsx or layout.tsx in src/routes/) - unexported or misplaced loaders/actions silently do nothing)
(You MUST NOT destructure store properties at the top level - destructuring breaks reactivity because you lose the Proxy reference)
</critical_requirements>
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
When to use:
- Building web apps where instant interactivity matters (zero hydration delay)
- Apps with complex interactivity that would ship too much JS with traditional hydration
- Projects needing fine-grained lazy loading without manual code-splitting
- Full-stack apps with server loaders, actions, and RPC via
server$ - Progressive enhancement where forms work without JavaScript
When NOT to use:
- Static content sites with minimal interactivity (use a static site generator)
- Projects where the team is deeply invested in React ecosystem libraries that have no Qwik equivalents
- Apps that rely heavily on non-serializable runtime state (class instances, closures with side effects)
Key patterns covered:
- Resumability mental model and the
$suffix convention - Component definition with
component$, props, and<Slot /> - Reactive state:
useSignal,useStore,useComputed$ - Lifecycle:
useTask$,useVisibleTask$,useResource$ - Event handling:
onClick$,preventdefault:click,sync$ - Qwik City routing: file-based routes, layouts, dynamic params
- Server data:
routeLoader$,routeAction$,server$ - Serialization rules and the
$boundary
Detailed Resources:
- For decision frameworks and anti-patterns, see reference.md
Core patterns:
- examples/core.md - Components, signals, stores, tasks, events, slots
- examples/routing.md - File-based routing, routeLoader$, routeAction$, server$, middleware
- examples/serialization.md - Serialization rules, $ boundary, non-serializable patterns
<philosophy>
Philosophy
Qwik is built on resumability - the idea that the server can serialize the entire application state (component tree, listeners, state) into HTML, and the client can resume exactly where the server left off without re-executing any framework code.
How it differs from hydration frameworks:
Traditional SSR frameworks render HTML on the server, then re-execute all component code on the client to attach event listeners and rebuild the component tree. This is hydration - the client replays the server's work.
Qwik skips this entirely. The server serializes everything into the HTML. When a user clicks a button, only the click handler's code downloads and executes. The framework itself, the component tree, and all other handlers stay unloaded until needed.
The $ suffix is the core mechanism. Every function ending in $ is a lazy-loading boundary. The Qwik optimizer splits code at each $ marker into separate chunks. This means:
component$()- the component's render function loads only when neededonClick$()- the click handler loads only when the user clicksrouteLoader$()- the loader runs server-side onlyuseTask$()- the task loads when its tracked dependencies change
The tradeoff: Because code must be serializable to cross $ boundaries, you cannot capture non-serializable values (class instances, functions, DOM nodes) in $ closures. This constraint is the price of instant interactivity.
When to use Qwik:
- Interactive apps where time-to-interactive matters
- Large apps where traditional hydration downloads too much JS upfront
- Full-stack apps leveraging
routeLoader$/routeAction$/server$for server logic - Progressive enhancement (Qwik forms work without JS)
When NOT to use Qwik:
- Static content sites with little interactivity
- Projects heavily dependent on React-specific libraries without Qwik equivalents
- Apps requiring extensive non-serializable runtime state
<patterns>
Core Patterns
Pattern 1: Components with component$
Every Qwik component must be wrapped in component$(). This is not optional - it enables lazy loading, hooks, and <Slot />.
import { component$, useSignal } from "@builder.io/qwik";
interface CounterProps {
initial?: number;
label: string;
}
export const Counter = component$<CounterProps>(({ initial = 0, label }) => {
const count = useSignal(initial);
return (
<div>
<span>
{label}: {count.value}
</span>
<button onClick$={() => count.value++}>+</button>
</div>
);
});
Why good: component$ enables the optimizer to split this into a lazy chunk, typed props via generic, useSignal for reactive state, onClick$ handler loads only on click
// BAD: Plain function component
export const Counter = (props: { label: string }) => {
// Cannot use hooks here - useSignal will throw
// Cannot use <Slot /> - only works inside component$
return <div>{props.label}</div>;
};
Why bad: Without component$ wrapper, hooks throw at runtime, <Slot /> breaks, optimizer cannot split the code, component is not resumable
Pattern 2: Reactive State - useSignal vs useStore
useSignal holds a single reactive value accessed via .value. useStore holds a reactive object with deep tracking by default.
import { component$, useSignal, useStore } from "@builder.io/qwik";
export const UserProfile = component$(() => {
// useSignal for primitives and flat values
const isEditing = useSignal(false);
const selectedTab = useSignal<"profile" | "settings">("profile");
// useStore for objects/arrays - deep reactivity by default
const user = useStore({
name: "Alice",
email: "alice@example.com",
preferences: {
theme: "dark",
notifications: true,
},
});
return (
<div>
<h1>{user.name}</h1>
{isEditing.value ? (
<input
value={user.name}
onInput$={(_, el) => {
user.name = el.value;
}}
/>
) : (
<button
onClick$={() => {
isEditing.value = true;
}}