Why Qwik Needs Rules That Prevent Hydration Patterns
Qwik is the only framework built around resumability — the page loads with zero JavaScript and lazily downloads only the code needed for the specific interaction the user triggers. There's no hydration step. AI assistants don't understand this model at all — they generate React/Next.js hydration patterns that force all component code to download on page load, defeating Qwik's entire value proposition.
The most critical AI failures: importing modules eagerly (breaking lazy loading), using React-style event handlers instead of $-suffixed handlers, generating useEffect-like patterns instead of Qwik's task system, and treating components as functions that re-execute (Qwik components execute once on the server and resume on the client without re-execution).
Qwik's $ suffix is the core concept AI must understand: any function suffixed with $ creates a lazy-loading boundary. The optimizer splits code at every $ — so event handlers, data loaders, and side effects are only downloaded when needed.
Rule 1: The $ Suffix and Lazy Loading
The rule: 'The $ suffix creates a lazy-loading boundary. Use it on: event handlers (onClick$), component definitions (component$), tasks (useTask$, useVisibleTask$), route loaders (routeLoader$), and actions (routeAction$). Every $ is a point where the optimizer can split code into a separate chunk that's only downloaded when needed. Never import eagerly what can be lazy-loaded with $.'
For event handlers: 'Always use $-suffixed handlers: onClick$={() => count.value++}. Never use plain onClick — it forces the handler code to be included in the initial bundle. The $ handler is only downloaded when the user actually clicks. This is Qwik's zero-JS-until-interaction model.'
For understanding: 'Think of $ as 'this function will be downloaded separately.' component$() creates a lazy component. routeLoader$() creates a lazy data loader. useTask$() creates a lazy side effect. The more code is behind $, the less JavaScript ships on initial page load.'
- $ = lazy boundary — code after $ downloads only when needed
- onClick$ for handlers — never plain onClick (forces eager download)
- component$ for components — routeLoader$ for data — routeAction$ for mutations
- useTask$ for side effects — useVisibleTask$ for DOM-dependent effects
- More $ = less initial JS = faster page load
Every $ suffix is a code-splitting point. onClick$ downloads the handler only when clicked. routeLoader$ downloads only when the route is visited. More $ = less initial JS = faster load. Think of $ as 'download separately.'
Rule 2: Signals for Reactive State
The rule: 'Use useSignal for primitive reactive state: const count = useSignal(0). Access and set with .value: count.value++. Use useStore for object state: const state = useStore({ name: "", items: [] }). Stores are deeply reactive — access properties directly, not .value. Use useComputed$ for derived values. Never use useState or useReducer — they don't exist in Qwik.'
For serialization: 'All state must be serializable — Qwik serializes state to HTML for resumability. No functions, Promises, DOM nodes, or class instances in signals or stores. If you need non-serializable data, use useVisibleTask$ to create it on the client side.'
For the difference from React: 'Qwik signals don't cause re-renders — they update only the specific DOM nodes that reference .value. This is fine-grained reactivity like Solid, not component-level like React. Never wrap components in memo() or useMemo — they don't exist and aren't needed.'
All Qwik state must be serializable — it's written to HTML for resumability. No functions, Promises, DOM nodes, or class instances in signals or stores. Non-serializable data crashes resumability.
Rule 3: Data Loading with routeLoader$
The rule: 'Use routeLoader$ for all route-level data loading. Route loaders run on the server and provide data to the page: export const useProducts = routeLoader$(async () => { return db.select().from(products); }). Access in components with useProducts(): const products = useProducts(). Loaders are cached per navigation — they don't re-run on client-side interactions.'
For dependent data: 'Use routeLoader$ with requestEvent for access to cookies, headers, and URL params: routeLoader$(async (requestEvent) => { const userId = requestEvent.cookie.get("userId"); }). Use redirect() for auth checks: if (!user) throw requestEvent.redirect(302, "/login").'
AI generates useEffect + fetch patterns from React. In Qwik, routeLoader$ is the equivalent of Next.js getServerSideProps — it runs on the server, provides data to the page, and is type-safe. Client-side fetching is only for user-initiated actions.
Rule 4: Mutations with routeAction$ and Form
The rule: 'Use routeAction$ for all data mutations: export const useAddTodo = routeAction$(async (data, requestEvent) => { ... }). Use Qwik City's <Form> component for form submissions: <Form action={addTodo}><input name="title" /><button>Add</button></Form>. Forms work without JavaScript (progressive enhancement). Use zod$ for server-side validation integrated with the action.'
For validation: 'Use zod$ as the second argument to routeAction$ for schema validation: routeAction$(handler, zod$({ title: z.string().min(1), done: z.boolean() })). Validation errors are automatically typed and available in the component via action.value?.fieldErrors.'
For optimistic UI: 'Use action.isRunning to show pending states. Use action.value for the action's return value. Use action.formData for optimistic access to the submitted form data before the action completes.'
- routeAction$ for mutations — Form component for submissions
- Progressive enhancement: forms work without JS
- zod$ for integrated server-side validation — typed errors
- action.isRunning for pending — action.formData for optimistic UI
- Never use fetch for form submissions — use routeAction$ + Form
Qwik City forms work without JavaScript — progressive enhancement built in. routeAction$ + <Form> gives you server-side mutation handling, typed validation with zod$, and optimistic UI when JS is available.
Rule 5: Qwik City Conventions
The rule: 'Qwik City is Qwik's meta-framework (like Next.js for React). Use file-based routing in src/routes/: index.tsx, about/index.tsx, blog/[slug]/index.tsx. Use layout.tsx for nested layouts. Use plugin@auth.ts for middleware. Use useLocation() for URL info, useNavigate() for programmatic navigation.'
For head management: 'Export a head property from routes for SEO: export const head: DocumentHead = { title: "My Page", meta: [{ name: "description", content: "..." }] }. Use useDocumentHead() in layouts to render head tags. For dynamic head, export a function: export const head: DocumentHead = ({ resolveValue }) => ({ title: resolveValue(useProduct).name }).'
For middleware: 'Use plugin@[name].ts files in the routes directory for middleware that runs before route handlers. Common use: auth checking, rate limiting, CORS headers. Middleware has access to requestEvent for cookies, headers, and redirect.'
Complete Qwik Rules Template
Consolidated rules for Qwik + Qwik City projects.
- $ suffix on all lazy boundaries: onClick$, component$, routeLoader$, routeAction$
- useSignal for primitives (.value) — useStore for objects (deep reactive) — all serializable
- routeLoader$ for data — runs on server, cached per navigation, typed access
- routeAction$ + Form for mutations — zod$ for validation — progressive enhancement
- File-based routing in src/routes/ — layout.tsx for nesting — plugin@*.ts for middleware
- DocumentHead export for SEO — useDocumentHead in layouts
- No hydration patterns — resumability means code downloads on interaction, not on load
- Vitest for unit tests — Playwright for e2e — $ aware testing patterns