creates a separate lazy-loadable chunk. The optimizer extracts these into individual bundles.\n\n### Step 3: Fine-Grained Reactivity\n```tsx\nexport default component$(() => {\n const count = useSignal(0)\n const name = useSignal('')\n\n const form = useStore({\n email: '',\n password: '',\n errors: {} as Record\u003cstring, string>,\n })\n\n const isFormValid = useComputed$(() =>\n form.email.includes('@') && form.password.length >= 8\n )\n\n return (\n \u003cdiv>\n \u003cp>Count: {count.value}\u003c/p>\n \u003cp>Form valid: {isFormValid.value}\u003c/p>\n \u003c/div>\n )\n})\n```\nReactivity is fine-grained — Qwik tracks reads at the property level, not via virtual DOM diffing.\n\n### Step 4: Qwik City Routing\n```\nsrc/routes/\n layout.tsx -> root layout\n index.tsx -> /\n about/index.tsx -> /about\n blog/\n layout.tsx -> blog layout\n index.tsx -> /blog\n [slug]/index.tsx -> /blog/:slug\n dashboard/\n layout.tsx -> authenticated layout\n index.tsx -> /dashboard\n api/\n users/index.ts -> /api/users (resource route)\n```\nAll routes, layouts, data loaders, and actions are lazy-loaded. Use `\u003cSlot />` for layout insertion points.\n\n### Step 5: Route Loaders and Actions\n```tsx\n// src/routes/dashboard/index.tsx\nimport { routeLoader$, routeAction$, Form } from '@builder.io/qwik-city'\n\nexport const useUserData = routeLoader$(async ({ cookie, redirect }) => {\n const token = cookie.get('token')?.value\n if (!token) throw redirect(302, '/login')\n const user = await db.user.findUnique({ where: { token } })\n return user as User\n})\n\nexport const useUpdateProfile = routeAction$(async (form, { fail }) => {\n const name = form.get('name')\n if (!name || name.length \u003c 2) return fail(400, { message: 'Name too short' })\n await db.user.update({ data: { name } })\n return { success: true }\n})\n\nexport default component$(() => {\n const user = useUserData()\n const action = useUpdateProfile()\n\n return (\n \u003cForm action={action}>\n \u003cinput name=\"name\" value={user.value.name} />\n {action.value?.failed && \u003cp>{action.value.fieldErrors?.message}\u003c/p>}\n \u003cbutton type=\"submit\">Update\u003c/button>\n \u003c/Form>\n )\n})\n```\n`routeLoader frontend-qwik-patterns · Skills-SI Market runs on server, serializes result into HTML. `routeAction frontend-qwik-patterns · Skills-SI Market handles form mutations with progressive enhancement.\n\n### Step 6: Prefetch Optimization\n```tsx\n// src/root.tsx\nimport { PrefetchServiceWorker } from '@builder.io/qwik/prefetch-service-worker'\n\nexport default component$(() => {\n return (\n \u003chtml>\n \u003chead>\n \u003cPrefetchServiceWorker />\n \u003c/head>\n \u003cbody>\n \u003cSlot />\n \u003c/body>\n \u003c/html>\n )\n})\n```\nPrefetchServiceWorker preloads QRL chunks for links in viewport, enabling instant navigation without waterfall.\n\n### Step 7: Server Functions\n```tsx\n// src/components/createUser.ts\nimport { server$ } from '@builder.io/qwik-city'\n\nexport const createUser = server$(async (data: CreateUserInput) => {\n const user = await db.user.create({ data })\n await email.sendWelcome(user.email)\n return user\n})\n```\n`server$()` marks a function to always run on the server. Callable from client components as if local.\n\n### Step 8: useVisibleTask$ for Client Effects\n```tsx\nexport default component$(() => {\n const elemRef = useSignal\u003cElement>()\n\n useVisibleTask$(() => {\n const observer = new IntersectionObserver((entries) => {\n entries.forEach(entry => {\n if (entry.isIntersecting) analytics.trackView(entry.target.id)\n })\n })\n if (elemRef.value) observer.observe(elemRef.value)\n return () => observer.disconnect()\n })\n\n return \u003cdiv ref={elemRef} id=\"tracked-element\">Content\u003c/div>\n})\n```\n\n## Component Architecture\n\n### Component Decision Tree\n```\nDoes the component need interactivity?\n No -> Static HTML, no component$() needed (plain function component)\n Yes -> Does it need state?\n No -> Use component$() with props only\n Yes -> Is the state simple (number, string)?\n Yes -> useSignal\n No -> useStore (object, array)\n\nDoes the component need side effects?\n No -> Skip useVisibleTask$\n Yes -> Is it a data fetch?\n No -> Is it DOM measurement, animation, or analytics?\n Yes -> useVisibleTask$ with proper cleanup\n Yes -> routeLoader$ in parent, pass as props\n```\n\n### Component Composition Patterns\n```tsx\n// Parent provides state, children receive props\nexport default component$(() => {\n const items = useSignal\u003cstring[]>([])\n return (\n \u003cdiv>\n \u003cItemList items={items.value} />\n \u003c/div>\n )\n})\n\n// Or use context for deep passing\nimport { createContextId, useContextProvider, useContext } from '@builder.io/qwik'\n\nexport const ThemeContext = createContextId\u003cstring>('theme')\n\nexport const Root = component$(() => {\n useContextProvider(ThemeContext, 'dark')\n return \u003cSlot />\n})\n\nexport const Child = component$(() => {\n const theme = useContext(ThemeContext)\n return \u003cdiv class={theme}>Content\u003c/div>\n})\n```\n\n### Inline Component Pattern (No $)\n\nFor non-interactive wrapper components that don't need lazy loading:\n\n```tsx\n// No component$() — this is an eager, lightweight wrapper\nexport function Container(props: { title: string; children?: any }) {\n return (\n \u003csection>\n \u003ch2>{props.title}\u003c/h2>\n {props.children}\n \u003c/section>\n )\n}\n```\n\n### Slot Pattern with Fallback\n\n```tsx\nexport const Card = component$(() => {\n return (\n \u003cdiv class=\"card\">\n \u003cdiv class=\"card-header\">\n \u003cSlot name=\"header\" />\n \u003c/div>\n \u003cdiv class=\"card-body\">\n \u003cSlot />\n \u003c/div>\n \u003c/div>\n )\n})\n```\n\n### Lazy-Loaded Third-Party Library\n\n```tsx\nexport default component$(() => {\n useVisibleTask$(async () => {\n const Chart = await import('chart.js')\n // Chart is only downloaded when this component becomes visible\n new Chart(ctx, { /* options */ })\n })\n return \u003ccanvas ref={canvasRef} />\n})\n```\n\n### Infinite Scroll Pattern\n\n```tsx\nexport default component$(() => {\n const items = useSignal\u003cItem[]>([])\n const page = useSignal(1)\n const loading = useSignal(false)\n\n const loadMore = $(async () => {\n if (loading.value) return\n loading.value = true\n const newItems = await fetch(`/api/items?page=${page.value}`).then(r => r.json())\n items.value = [...items.value, ...newItems]\n page.value++\n loading.value = false\n })\n\n return (\n \u003cdiv>\n {items.value.map(item => \u003cItemCard item={item} />)}\n \u003cbutton onClick$={loadMore}>\n {loading.value ? 'Loading...' : 'Load More'}\n \u003c/button>\n \u003c/div>\n )\n})\n```\n\n### Debounced Search Pattern\n\n```tsx\nexport default component$(() => {\n const query = useSignal('')\n const results = useSignal\u003cSearchResult[]>([])\n\n const search = $(async (q: string) => {\n if (q.length \u003c 2) { results.value = []; return }\n results.value = await fetch(`/api/search?q=${q}`).then(r => r.json())\n })\n\n return (\n \u003cdiv>\n \u003cinput\n onInput$={async (_, el) => {\n query.value = el.value\n await search(el.value)\n }}\n placeholder=\"Search...\"\n />\n \u003cul>\n {results.value.map(r => \u003cli key={r.id}>{r.title}\u003c/li>)}\n \u003c/ul>\n \u003c/div>\n )\n})\n```\n\n## State Management Patterns\n\n### useSignal for Primitives\n```tsx\nconst count = useSignal(0) // number\nconst name = useSignal('') // string\nconst isActive = useSignal(true) // boolean\nconst items = useSignal\u003cstring[]>([]) // array (replaced entirely)\n```\n\n### useStore for Objects\n```tsx\nconst user = useStore({\n id: '',\n name: '',\n email: '',\n preferences: { theme: 'light', language: 'en' },\n metadata: { createdAt: null as Date | null },\n})\n```\nQwik tracks property-level reads. Mutations like `user.preferences.theme = 'dark'` trigger targeted re-renders without VDOM diffing.\n\n### Derived State\n```tsx\nconst firstName = useSignal('John')\nconst lastName = useSignal('Doe')\n\nconst fullName = useComputed$(() =>\n `${firstName.value} ${lastName.value}`\n)\n// fullName.value updates automatically when firstName or lastName changes\n```\n\n### Context-Based Shared State\n```tsx\n// Define context type\ninterface AuthState { user: User | null; login: (email: string, password: string) => Promise\u003cvoid> }\n\n// Create context ID\nexport const AuthContext = createContextId\u003cAuthState>('auth')\n\n// Provider in root layout\nexport const Root = component$(() => {\n const authState = useStore\u003cAuthState>({\n user: null,\n login: $(async (email, password) => {\n const user = await loginUser(email, password)\n authState.user = user\n }),\n })\n useContextProvider(AuthContext, authState)\n return \u003cSlot />\n})\n\n// Consumer in any child\nexport const Profile = component$(() => {\n const auth = useContext(AuthContext)\n return \u003cdiv>{auth.user ? `Welcome ${auth.user.name}` : 'Not logged in'}\u003c/div>\n})\n```\n\n### Server State Synchronization\nUse routeLoader$ + routeAction$ for server state. The client can invalidate loaders after an action:\n\n```tsx\nexport const usePosts = routeLoader$(async () => {\n return await db.post.findMany() as Post[]\n})\n\nexport const useCreatePost = routeAction$(async (form) => {\n await db.post.create({ data: { title: form.get('title') } })\n // Loader automatically re-runs on next request\n})\n\nexport default component$(() => {\n const posts = usePosts()\n const createPost = useCreatePost()\n return \u003cForm action={createPost}>...\u003c/Form>\n})\n```\n\n### URL as State Source\n\n```tsx\nexport default component$(() => {\n const loc = useLocation()\n const nav = useNavigate()\n\n // Read state from URL\n const currentPage = Number(loc.params.page) || 1\n const sortBy = loc.url.searchParams.get('sort') || 'date'\n\n // Update URL\n return \u003cbutton onClick$={() => nav(`/list?sort=${sortBy}&page=${currentPage + 1}`)}>Next\u003c/button>\n})\n```\n\n## Performance Optimization\n\n1. Initial load: ~10KB JS baseline, zero hydration cost, instant TTI.\n2. Lazy chunks: Each $() is ~200-500 bytes, loaded on interaction only.\n3. Serialized state: HTML size increases by ~2-5KB per page with serialized signals.\n4. Prefetch: PrefetchServiceWorker uses idle time to download route chunks.\n5. Bundle splitting: Automatic at the $ boundary — no manual code splitting.\n6. Memory: Fine-grained signals use less memory than virtual DOM tree.\n7. Time to interactive: Near zero because there is no hydration to wait for.\n\n### Chunk Size Budgets\n- Each $ boundary: 200-500 bytes target\n- Route loader data: \u003c 10KB serialized per page\n- Total initial JS: \u003c 15KB\n- Total per-interaction JS: \u003c 5KB\n\n### Measuring Performance\n```bash\nnpx qwik build --analyze # opens bundle analyzer\nnpm run qwik build && node dist/server/entry.mjs # test SSR perf\n```\n\n## Build & Bundle Considerations\n\n### Entry Strategies\n\n| Strategy | Chunks | Use Case |\n|----------|--------|----------|\n| `smart` | Many small chunks (default) | Most applications — optimal lazy loading |\n| `hoist` | Fewer, larger chunks | Apps with many shared dependencies |\n| `single` | One bundle | Small apps, no lazy benefit needed |\n\nConfigure in `vite.config.ts`:\n```ts\nqwikVite({ entryStrategy: { type: 'hoist' } })\n```\n\n### Tree Shaking\nQwik's optimizer automatically tree-shakes unused exports from `$()` boundaries. Only code reachable from a `$()` boundary is included in the client bundle.\n\n### CSS Bundling\n- CSS imported in components is automatically extracted and split\n- Global CSS in `src/global.css` is inlined in the initial HTML\n- Use CSS Modules for component-scoped styles: `import styles from './component.module.css'`\n\n### Adapter-Specific Builds\n\n```ts\n// Cloudflare Pages\nimport cloudflarePages from '@builder.io/qwik-city/adapters/cloudflare-pages/vite'\n// Node.js server\nimport nodeServer from '@builder.io/qwik-city/adapters/node-server/vite'\n// Vercel Edge\nimport vercelEdge from '@builder.io/qwik-city/adapters/vercel-edge/vite'\n```\n\nEach adapter changes the SSR entry point and output format.\n\n## Testing Strategies\n\n### Component Unit Tests\n\n```tsx\n// __tests__/counter.test.tsx\nimport { createDOM } from '@builder.io/qwik/testing'\nimport { test, expect } from 'vitest'\nimport Counter from '../src/components/counter'\n\ntest('should increment', async () => {\n const { screen, render, userEvent } = await createDOM()\n await render(\u003cCounter />)\n expect(screen.querySelector('button')?.innerHTML).toBe('0')\n await userEvent('button', 'click')\n expect(screen.querySelector('button')?.innerHTML).toBe('1')\n})\n```\n\n### Signal/Store Logic Tests\n\n```tsx\n// __tests__/state.test.ts\nimport { test, expect } from 'vitest'\nimport { useSignal, useStore } from '@builder.io/qwik'\n\ntest('signal updates reactively', () => {\n const count = useSignal(0)\n expect(count.value).toBe(0)\n count.value = 5\n expect(count.value).toBe(5)\n})\n\ntest('store tracks nested mutations', () => {\n const state = useStore({ user: { name: 'John' } })\n expect(state.user.name).toBe('John')\n state.user.name = 'Jane'\n expect(state.user.name).toBe('Jane')\n})\n```\n\n### Route Loader Tests\n\n```tsx\n// __tests__/loaders.test.ts\nimport { test, expect } from 'vitest'\nimport { useProductData } from '../src/routes/product/[id]'\n\ntest('loader fetches product by id', async () => {\n const mockContext = {\n params: { id: '1' },\n request: new Request('http://localhost/product/1'),\n // ... other context properties\n }\n const result = await useProductData(mockContext)\n expect(result).toHaveProperty('name')\n expect(result).toHaveProperty('price')\n})\n```\n\n### E2E Testing\n\n```tsx\n// e2e/navigation.spec.ts\nimport { test, expect } from '@playwright/test'\n\ntest('navigates without full page reload', async ({ page }) => {\n await page.goto('/')\n await page.click('a[href=\"/about\"]')\n await expect(page.locator('h1')).toHaveText('About')\n // Verify client-side navigation (no full page reload)\n})\n\ntest('form submission with validation', async ({ page }) => {\n await page.goto('/dashboard')\n await page.fill('input[name=\"name\"]', 'A')\n await page.click('button[type=\"submit\"]')\n await expect(page.locator('.error')).toContainText('too short')\n})\n```\n\n## Migration Patterns\n\n### React to Qwik Migration Plan\n\n**Phase 1 — Audit existing components:**\n- Identify which components need client interactivity\n- Map React hooks to Qwik equivalents\n- Identify server-side data dependencies\n\n**Phase 2 — Rewrite leaf components:**\n```tsx\n// React\nfunction Toggle() {\n const [on, setOn] = useState(false)\n return \u003cbutton onClick={() => setOn(!on)}>{on ? 'ON' : 'OFF'}\u003c/button>\n}\n\n// Qwik\nexport default component$(() => {\n const on = useSignal(false)\n return \u003cbutton onClick$={() => on.value = !on.value}>{on.value ? 'ON' : 'OFF'}\u003c/button>\n})\n```\n\n**Phase 3 — Migrate routing and data:**\n- Replace React Router with Qwik City file-based routing\n- Replace React Query/useEffect fetches with routeLoader$\n- Replace API routes with resource routes or server$\n\n**Phase 4 — Enable prefetch and optimize:**\n- Add PrefetchServiceWorker\n- Verify $ boundary coverage with Qwik DevTools\n- Audit chunk sizes with `qwik build --analyze`\n\n### Next.js to Qwik City\n\n| Next.js | Qwik City |\n|---------|-----------|\n| `pages/[slug].tsx` | `src/routes/[slug]/index.tsx` |\n| `getServerSideProps` | `routeLoader frontend-qwik-patterns · Skills-SI Market |\n| `getStaticProps` | `routeLoader frontend-qwik-patterns · Skills-SI Market with SSG |\n| Server Actions | `routeAction frontend-qwik-patterns · Skills-SI Market |\n| `middleware.ts` | `plugin@name.ts` |\n| `layout.tsx` | `layout.tsx` per segment |\n| `next/image` | `qwik-image` or `\u003cimg>` with lazy loading |\n\n### Incremental Strategy\n1. **Route-level** — Migrate one route at a time, keeping old and new coexisting\n2. **Component-level** — Embed Qwik widgets in existing pages via web components\n3. **Feature-level** — Build new features in Qwik, leave legacy features in old framework\n\n## Anti-Patterns\n\n### Missing $ Suffix\n```tsx\n// Anti-pattern: onClick without $ — handler is eager, not lazy\n\u003cbutton onClick={() => doSomething()}>Click\u003c/button>\n\n// Correct\n\u003cbutton onClick$={() => doSomething()}>Click\u003c/button>\n```\n\n### State Outside Qwik's Reactivity System\n```tsx\n// Anti-pattern: plain variable — changes won't trigger re-render\nlet count = 0\n\n// Correct: useSignal\nconst count = useSignal(0)\n```\n\n### Data Fetching in useVisibleTask$\n```tsx\n// Anti-pattern: useVisibleTask$ for data\nuseVisibleTask$(async () => {\n const data = await fetch('/api/data').then(r => r.json())\n state.value = data\n})\n\n// Correct: routeLoader$\nexport const useData = routeLoader$(async () => {\n return await fetch('/api/data').then(r => r.json())\n})\n```\n\n### Large Serialized State\n```tsx\n// Anti-pattern: serializing entire data set\nconst allProducts = useSignal(await fetch('/api/products/10000').then(r => r.json()))\n\n// Correct: lazy load, paginate\nexport const useProducts = routeLoader$(async ({ query }) => {\n const page = Number(query.get('page')) || 1\n return await db.product.findMany({ skip: (page-1)*20, take: 20 })\n})\n```\n\n### Mixing React and Qwik in Same Component\nDon't import or use React hooks inside a Qwik component. The reactivity systems are incompatible.\n\n### Over-Serialization\n```tsx\n// Anti-pattern: storing non-serializable values in signals\nconst chartInstance = useSignal\u003cChart | null>(null) // Chart is a class instance\n\n// Correct: use NoSerialize wrapper\nimport { NoSerialize } from '@builder.io/qwik'\nconst chartInstance = useSignal\u003cNoSerialize\u003cChart>>()\n```\n\n## Common Pitfalls\n\n1. **Forgetting $ on event handlers**: `onClick` instead of `onClick frontend-qwik-patterns · Skills-SI Market — event never gets lazy loaded.\n2. **Using useState from React**: Qwik uses useSignal and useStore, not hooks from other frameworks.\n3. **Heavy useVisibleTask$ usage**: Every useVisibleTask$ blocks resumability. Keep them minimal.\n4. **Dynamic $() calls**: `$(someCondition ? fn1 : fn2)` breaks the optimizer. Use static boundaries.\n5. **Missing PrefetchServiceWorker**: Without it, lazy navigation produces visible network waterfalls.\n6. **Mutable state reassignment**: `state = newValue` instead of `state.value = newValue` for signals.\n7. **Nested reactivity with useStore**: Objects inside useStore are deeply reactive — use for form state.\n8. **Not using typed loaders**: routeLoader$ returns `unknown` without explicit typing.\n\n## Best Practices\n\n1. Every `$()` should be statically analyzable — no dynamic code inside dollar boundaries.\n2. `useSignal` for primitives, `useStore` for objects. Never plain `let` or `const`.\n3. Colocate routeLoader$ and routeAction$ in the route file that uses them.\n4. Use `useComputed frontend-qwik-patterns · Skills-SI Market for derived values — avoids manual synchronization.\n5. Keep useVisibleTask$ focused on a single concern — one observer per task.\n6. Lazy-load heavy libraries inside `$()` callbacks, not at module level.\n7. Use `NoSerialize` wrapper for non-serializable values (class instances, DOM refs).\n8. Prefetch critical routes after initial render with PrefetchServiceWorker.\n\n## Compared With\n\n| Aspect | Qwik | React (Client) | Solid |\n|--------|------|----------------|-------|\n| Rendering | Resumable | Hydrating | Hydrating |\n| Lazy loading | Per-event ($) | Per-component (lazy()) | Per-component (lazy()) |\n| Reactivity | Signal-based, fine-grained | Virtual DOM diff | Signal-based |\n| Serialization | Automatic via QRL | Manual (JSON) | Manual |\n| Server functions | Built-in (server$) | Via Server Actions | Via SolidStart |\n| Bundle size | ~10KB (initial) | ~40KB+ (React runtime) | ~8KB (initial) |\n\n## Tooling\n\n1. `npm run qwik build` — production build with Qwik optimizer.\n2. `npm run qwik dev` — dev server with HMR and the Qwik DevTools panel.\n3. `npm run qwik lint` — ESLint with Qwik-specific rules (ensures $ boundaries).\n4. `npm run qwik qwik add` — CLI to add integrations (auth, database, UI).\n5. Qwik DevTools browser extension — inspect lazy boundaries and QRL chunks.\n6. `@builder.io/qwik-labs` — experimental features and utilities.\n7. `qwik-speak` — internationalization library for Qwik.\n8. `qwik-image` — optimized images with lazy loading.\n\n## Rules\n- No eager code — every component, event, and closure is lazy by default via ` frontend-qwik-patterns · Skills-SI Market .\n- `useSignal` for primitives, `useStore` for objects. Never use plain `let` or `const` for reactive state.\n- `routeLoader frontend-qwik-patterns · Skills-SI Market for data fetching, `routeAction frontend-qwik-patterns · Skills-SI Market with `\u003cForm>` for mutations.\n- `useVisibleTask frontend-qwik-patterns · Skills-SI Market only for client-only effects (intersection observers, analytics) — avoid for data fetching.\n- PrefetchServiceWorker is required for instant navigation — without it, lazy loading becomes waterfall.\n- Qwik optimizer must be able to see `$()` boundaries — no dynamic ` frontend-qwik-patterns · Skills-SI Market calls.\n- Serialized state stays in HTML — never rely on sessionStorage for critical app state.\n- Dollar boundaries must be statically analyzable — no dynamic expressions inside $().\n\n## References\n - references/qwik-city-patterns.md — Qwik City Patterns\n - references/qwik-component-patterns.md — Qwik Component Patterns\n - references/qwik-data.md — Qwik Data Patterns\n - references/qwik-routing.md — Qwik Routing Patterns\n - references/qwik-state-management.md — Qwik State Management\n - references/resumable-patterns.md — Qwik Resumable Patterns\n - references/qwik-component-composition.md — Qwik Component Composition Reference\n - references/qwik-form-validation.md — Qwik Form Validation Reference\n\n## Handoff\nNo artifact produced.\nNext skill: frontend-universal-testing for Qwik component tests. Or frontend-universal-performance.\nCarry forward: resumability boundaries, dollar-sign conventions, prefetch strategy.\n","licenseDocuments":[]}}
L’interface est localisée ; les présentations et documents des Skills sont traduits automatiquement par IA dans votre langue. Le code, les commandes, chemins et termes techniques restent en version originale, et le document de l’auteur fait foi.
Explorer les Skills
Design et création · SKILL.md

frontend-qwik-patterns

frontend-qwik-patterns

Use this skill when the user says 'Qwik pattern', 'Qwik resumable', 'Qwik City route', 'useSignal', 'useStore', 'Qwik lazy loading', 'Qwik optimizer', '$()', 'component$()', 'routeLoader#x27;, 'routeAction#x27;. This skill enforces: resumability over hydration, dollar-sign API for all lazy boundaries, fine-grained reactivity with useSignal/useStore, Qwik City fil…

Independent GitHub snapshot · Automated classification · Original documentation governs

Documentation de la Skill

GitHub

name: frontend-qwik-patterns description: > Use this skill when the user says 'Qwik pattern', 'Qwik resumable', 'Qwik City route', 'useSignal', 'useStore', 'Qwik lazy loading', 'Qwik optimizer', '$()', 'component$()', 'routeLoader#x27;, 'routeAction#x27;. This skill enforces: resumability over hydration, dollar-sign API for all lazy boundaries, fine-grained reactivity with useSignal/useStore, Qwik City file-based routing with lazy loaders/actions, and optimistic prefetching via PrefetchServiceWorker. Requires Qwik project (package.json with @builder.io/qwik). Do NOT use for: Qwik project setup/architecture, or non-Qwik frameworks. version: "1.0.0" author: "j4flmao" license: "MIT" compatibility: claude-code: true cursor: true codex: true windsurf: true tags: [frontend, qwik, patterns, phase-10]

Qwik Patterns

Purpose

Apply production patterns to Qwik applications: resumability-driven architecture, fine-grained reactivity, lazy loading boundaries, Qwik City routing with server functions, and prefetch optimization.

Agent Protocol

Trigger

Exact user phrases: "Qwik pattern", "Qwik resumable", "Qwik City", "useSignal", "useStore", "Qwik lazy loading", "Qwik optimizer", "$()", "component$()", "routeLoaderquot;, "routeActionquot;.

Input Context

Before activating, verify:

  • package.json has @builder.io/qwik and @builder.io/qwik-city.
  • Vite config has Qwik plugins.
  • Prefetch strategy is configured (PrefetchServiceWorker).

Output Artifact

No file output. Produces resumability plan, component patterns, route design as text.

Response Format

Resumability Plan: {lazy boundary map}
Component Pattern: {component$ / $() usage}
Route Design: {loaders + actions + layout}

No preamble. No postamble. No explanations. No filler/hedging/transitions. Compress output — why use many token when few do trick.

Completion Criteria

  • All components use component$() for lazy loading.
  • Event handlers use $ suffix (onClick$, onInput$).
  • Closures passed across lazy boundaries use $().
  • State uses useSignal (primitive) or useStore (object).
  • Data loading uses routeLoader$() with typed returns.
  • Mutations use routeAction$() with Form component.
  • PrefetchServiceWorker enabled in root layout.
  • useVisibleTask$ used sparingly and only for client-only effects.

Max Response Length

2560 tokens.

Workflow

Step 1: Resumability Pattern

export default component$(() => {
  const count = useSignal(0)

  return (
    <button onClick$={() => count.value++}>
      {count.value}
    </button>
  )
})

No hydration: HTML contains serialized state (data-qwik) + QRL references to lazy-loadable chunks. Browser resumes execution without replaying component tree.

Step 2: Dollar Sign API Boundaries

export default component$(() => {
  const state = useStore({ items: [], filter: '' })

  const log = $((msg: string) => {
    console.log(msg, state.items)
  })

  return (
    <input onInput$={(_, el) => state.filter = el.value} />
  )
})

Every $ creates a separate lazy-loadable chunk. The optimizer extracts these into individual bundles.

Step 3: Fine-Grained Reactivity

export default component$(() => {
  const count = useSignal(0)
  const name = useSignal('')

  const form = useStore({
    email: '',
    password: '',
    errors: {} as Record<string, string>,
  })

  const isFormValid = useComputed$(() =>
    form.email.includes('@') && form.password.length >= 8
  )

  return (
    <div>
      <p>Count: {count.value}</p>
      <p>Form valid: {isFormValid.value}</p>
    </div>
  )
})

Reactivity is fine-grained — Qwik tracks reads at the property level, not via virtual DOM diffing.

Step 4: Qwik City Routing

src/routes/
  layout.tsx                    ->  root layout
  index.tsx                     ->  /
  about/index.tsx               ->  /about
  blog/
    layout.tsx                  ->  blog layout
    index.tsx                   ->  /blog
    [slug]/index.tsx            ->  /blog/:slug
  dashboard/
    layout.tsx                  ->  authenticated layout
    index.tsx                   ->  /dashboard
  api/
    users/index.ts              ->  /api/users (resource route)

All routes, layouts, data loaders, and actions are lazy-loaded. Use <Slot /> for layout insertion points.

Step 5: Route Loaders and Actions

// src/routes/dashboard/index.tsx
import { routeLoader$, routeAction$, Form } from '@builder.io/qwik-city'

export const useUserData = routeLoader$(async ({ cookie, redirect }) => {
  const token = cookie.get('token')?.value
  if (!token) throw redirect(302, '/login')
  const user = await db.user.findUnique({ where: { token } })
  return user as User
})

export const useUpdateProfile = routeAction$(async (form, { fail }) => {
  const name = form.get('name')
  if (!name || name.length < 2) return fail(400, { message: 'Name too short' })
  await db.user.update({ data: { name } })
  return { success: true }
})

export default component$(() => {
  const user = useUserData()
  const action = useUpdateProfile()

  return (
    <Form action={action}>
      <input name="name" value={user.value.name} />
      {action.value?.failed && <p>{action.value.fieldErrors?.message}</p>}
      <button type="submit">Update</button>
    </Form>
  )
})

routeLoader$ runs on server, serializes result into HTML. routeAction$ handles form mutations with progressive enhancement.

Step 6: Prefetch Optimization

// src/root.tsx
import { PrefetchServiceWorker } from '@builder.io/qwik/prefetch-service-worker'

export default component$(() => {
  return (
    <html>
      <head>
        <PrefetchServiceWorker />
      </head>
      <body>
        <Slot />
      </body>
    </html>
  )
})

PrefetchServiceWorker preloads QRL chunks for links in viewport, enabling instant navigation without waterfall.

Step 7: Server Functions

// src/components/createUser.ts
import { server$ } from '@builder.io/qwik-city'

export const createUser = server$(async (data: CreateUserInput) => {
  const user = await db.user.create({ data })
  await email.sendWelcome(user.email)
  return user
})

server$() marks a function to always run on the server. Callable from client components as if local.

Step 8: useVisibleTask$ for Client Effects

export default component$(() => {
  const elemRef = useSignal<Element>()

  useVisibleTask$(() => {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) analytics.trackView(entry.target.id)
      })
    })
    if (elemRef.value) observer.observe(elemRef.value)
    return () => observer.disconnect()
  })

  return <div ref={elemRef} id="tracked-element">Content</div>
})

Component Architecture

Component Decision Tree

Does the component need interactivity?
  No  -> Static HTML, no component$() needed (plain function component)
  Yes -> Does it need state?
    No  -> Use component$() with props only
    Yes -> Is the state simple (number, string)?
      Yes -> useSignal
      No -> useStore (object, array)

Does the component need side effects?
  No -> Skip useVisibleTask$
  Yes -> Is it a data fetch?
    No -> Is it DOM measurement, animation, or analytics?
      Yes -> useVisibleTask$ with proper cleanup
    Yes -> routeLoader$ in parent, pass as props

Component Composition Patterns

// Parent provides state, children receive props
export default component$(() => {
  const items = useSignal<string[]>([])
  return (
    <div>
      <ItemList items={items.value} />
    </div>
  )
})

// Or use context for deep passing
import { createContextId, useContextProvider, useContext } from '@builder.io/qwik'

export const ThemeContext = createContextId<string>('theme')

export const Root = component$(() => {
  useContextProvider(ThemeContext, 'dark')
  return <Slot />
})

export const Child = component$(() => {
  const theme = useContext(ThemeContext)
  return <div class={theme}>Content</div>
})

Inline Component Pattern (No $)

For non-interactive wrapper components that don't need lazy loading:

// No component$() — this is an eager, lightweight wrapper
export function Container(props: { title: string; children?: any }) {
  return (
    <section>
      <h2>{props.title}</h2>
      {props.children}
    </section>
  )
}

Slot Pattern with Fallback

export const Card = component$(() => {
  return (
    <div class="card">
      <div class="card-header">
        <Slot name="header" />
      </div>
      <div class="card-body">
        <Slot />
      </div>
    </div>
  )
})

Lazy-Loaded Third-Party Library

export default component$(() => {
  useVisibleTask$(async () => {
    const Chart = await import('chart.js')
    // Chart is only downloaded when this component becomes visible
    new Chart(ctx, { /* options */ })
  })
  return <canvas ref={canvasRef} />
})

Infinite Scroll Pattern

export default component$(() => {
  const items = useSignal<Item[]>([])
  const page = useSignal(1)
  const loading = useSignal(false)

  const loadMore = $(async () => {
    if (loading.value) return
    loading.value = true
    const newItems = await fetch(`/api/items?page=${page.value}`).then(r => r.json())
    items.value = [...items.value, ...newItems]
    page.value++
    loading.value = false
  })

  return (
    <div>
      {items.value.map(item => <ItemCard item={item} />)}
      <button onClick$={loadMore}>
        {loading.value ? 'Loading...' : 'Load More'}
      </button>
    </div>
  )
})

Debounced Search Pattern

export default component$(() => {
  const query = useSignal('')
  const results = useSignal<SearchResult[]>([])

  const search = $(async (q: string) => {
    if (q.length < 2) { results.value = []; return }
    results.value = await fetch(`/api/search?q=${q}`).then(r => r.json())
  })

  return (
    <div>
      <input
        onInput$={async (_, el) => {
          query.value = el.value
          await search(el.value)
        }}
        placeholder="Search..."
      />
      <ul>
        {results.value.map(r => <li key={r.id}>{r.title}</li>)}
      </ul>
    </div>
  )
})

State Management Patterns

useSignal for Primitives

const count = useSignal(0)          // number
const name = useSignal('')          // string
const isActive = useSignal(true)    // boolean
const items = useSignal<string[]>([])  // array (replaced entirely)

useStore for Objects

const user = useStore({
  id: '',
  name: '',
  email: '',
  preferences: { theme: 'light', language: 'en' },
  metadata: { createdAt: null as Date | null },
})

Qwik tracks property-level reads. Mutations like user.preferences.theme = 'dark' trigger targeted re-renders without VDOM diffing.

Derived State

const firstName = useSignal('John')
const lastName = useSignal('Doe')

const fullName = useComputed$(() =>
  `${firstName.value} ${lastName.value}`
)
// fullName.value updates automatically when firstName or lastName changes

Context-Based Shared State

// Define context type
interface AuthState { user: User | null; login: (email: string, password: string) => Promise<void> }

// Create context ID
export const AuthContext = createContextId<AuthState>('auth')

// Provider in root layout
export const Root = component$(() => {
  const authState = useStore<AuthState>({
    user: null,
    login: $(async (email, password) => {
      const user = await loginUser(email, password)
      authState.user = user
    }),
  })
  useContextProvider(AuthContext, authState)
  return <Slot />
})

// Consumer in any child
export const Profile = component$(() => {
  const auth = useContext(AuthContext)
  return <div>{auth.user ? `Welcome ${auth.user.name}` : 'Not logged in'}</div>
})

Server State Synchronization

Use routeLoader$ + routeAction$ for server state. The client can invalidate loaders after an action:

export const usePosts = routeLoader$(async () => {
  return await db.post.findMany() as Post[]
})

export const useCreatePost = routeAction$(async (form) => {
  await db.post.create({ data: { title: form.get('title') } })
  // Loader automatically re-runs on next request
})

export default component$(() => {
  const posts = usePosts()
  const createPost = useCreatePost()
  return <Form action={createPost}>...</Form>
})

URL as State Source

export default component$(() => {
  const loc = useLocation()
  const nav = useNavigate()

  // Read state from URL
  const currentPage = Number(loc.params.page) || 1
  const sortBy = loc.url.searchParams.get('sort') || 'date'

  // Update URL
  return <button onClick$={() => nav(`/list?sort=${sortBy}&page=${currentPage + 1}`)}>Next</button>
})

Performance Optimization

  1. Initial load: ~10KB JS baseline, zero hydration cost, instant TTI.
  2. Lazy chunks: Each $() is ~200-500 bytes, loaded on interaction only.
  3. Serialized state: HTML size increases by ~2-5KB per page with serialized signals.
  4. Prefetch: PrefetchServiceWorker uses idle time to download route chunks.
  5. Bundle splitting: Automatic at the $ boundary — no manual code splitting.
  6. Memory: Fine-grained signals use less memory than virtual DOM tree.
  7. Time to interactive: Near zero because there is no hydration to wait for.

Chunk Size Budgets

  • Each $ boundary: 200-500 bytes target
  • Route loader data: < 10KB serialized per page
  • Total initial JS: < 15KB
  • Total per-interaction JS: < 5KB

Measuring Performance

npx qwik build --analyze  # opens bundle analyzer
npm run qwik build && node dist/server/entry.mjs  # test SSR perf

Build & Bundle Considerations

Entry Strategies

StrategyChunksUse Case
smartMany small chunks (default)Most applications — optimal lazy loading
hoistFewer, larger chunksApps with many shared dependencies
singleOne bundleSmall apps, no lazy benefit needed

Configure in vite.config.ts:

qwikVite({ entryStrategy: { type: 'hoist' } })

Tree Shaking

Qwik's optimizer automatically tree-shakes unused exports from $() boundaries. Only code reachable from a $() boundary is included in the client bundle.

CSS Bundling

  • CSS imported in components is automatically extracted and split
  • Global CSS in src/global.css is inlined in the initial HTML
  • Use CSS Modules for component-scoped styles: import styles from './component.module.css'

Adapter-Specific Builds

// Cloudflare Pages
import cloudflarePages from '@builder.io/qwik-city/adapters/cloudflare-pages/vite'
// Node.js server
import nodeServer from '@builder.io/qwik-city/adapters/node-server/vite'
// Vercel Edge
import vercelEdge from '@builder.io/qwik-city/adapters/vercel-edge/vite'

Each adapter changes the SSR entry point and output format.

Testing Strategies

Component Unit Tests

// __tests__/counter.test.tsx
import { createDOM } from '@builder.io/qwik/testing'
import { test, expect } from 'vitest'
import Counter from '../src/components/counter'

test('should increment', async () => {
  const { screen, render, userEvent } = await createDOM()
  await render(<Counter />)
  expect(screen.querySelector('button')?.innerHTML).toBe('0')
  await userEvent('button', 'click')
  expect(screen.querySelector('button')?.innerHTML).toBe('1')
})

Signal/Store Logic Tests

// __tests__/state.test.ts
import { test, expect } from 'vitest'
import { useSignal, useStore } from '@builder.io/qwik'

test('signal updates reactively', () => {
  const count = useSignal(0)
  expect(count.value).toBe(0)
  count.value = 5
  expect(count.value).toBe(5)
})

test('store tracks nested mutations', () => {
  const state = useStore({ user: { name: 'John' } })
  expect(state.user.name).toBe('John')
  state.user.name = 'Jane'
  expect(state.user.name).toBe('Jane')
})

Route Loader Tests

// __tests__/loaders.test.ts
import { test, expect } from 'vitest'
import { useProductData } from '../src/routes/product/[id]'

test('loader fetches product by id', async () => {
  const mockContext = {
    params: { id: '1' },
    request: new Request('http://localhost/product/1'),
    // ... other context properties
  }
  const result = await useProductData(mockContext)
  expect(result).toHaveProperty('name')
  expect(result).toHaveProperty('price')
})

E2E Testing

// e2e/navigation.spec.ts
import { test, expect } from '@playwright/test'

test('navigates without full page reload', async ({ page }) => {
  await page.goto('/')
  await page.click('a[href="/about"]')
  await expect(page.locator('h1')).toHaveText('About')
  // Verify client-side navigation (no full page reload)
})

test('form submission with validation', async ({ page }) => {
  await page.goto('/dashboard')
  await page.fill('input[name="name"]', 'A')
  await page.click('button[type="submit"]')
  await expect(page.locator('.error')).toContainText('too short')
})

Migration Patterns

React to Qwik Migration Plan

Phase 1 — Audit existing components:

  • Identify which components need client interactivity
  • Map React hooks to Qwik equivalents
  • Identify server-side data dependencies

Phase 2 — Rewrite leaf components:

// React
function Toggle() {
  const [on, setOn] = useState(false)
  return <button onClick={() => setOn(!on)}>{on ? 'ON' : 'OFF'}</button>
}

// Qwik
export default component$(() => {
  const on = useSignal(false)
  return <button onClick$={() => on.value = !on.value}>{on.value ? 'ON' : 'OFF'}</button>
})

Phase 3 — Migrate routing and data:

  • Replace React Router with Qwik City file-based routing
  • Replace React Query/useEffect fetches with routeLoader$
  • Replace API routes with resource routes or server$

Phase 4 — Enable prefetch and optimize:

  • Add PrefetchServiceWorker
  • Verify $ boundary coverage with Qwik DevTools
  • Audit chunk sizes with qwik build --analyze

Next.js to Qwik City

Next.jsQwik City
pages/[slug].tsxsrc/routes/[slug]/index.tsx
getServerSidePropsrouteLoader$
getStaticPropsrouteLoader$ with SSG
Server ActionsrouteAction$
middleware.tsplugin@name.ts
layout.tsxlayout.tsx per segment
next/imageqwik-image or <img> with lazy loading

Incremental Strategy

  1. Route-level — Migrate one route at a time, keeping old and new coexisting
  2. Component-level — Embed Qwik widgets in existing pages via web components
  3. Feature-level — Build new features in Qwik, leave legacy features in old framework

Anti-Patterns

Missing $ Suffix

// Anti-pattern: onClick without $ — handler is eager, not lazy
<button onClick={() => doSomething()}>Click</button>

// Correct
<button onClick$={() => doSomething()}>Click</button>

State Outside Qwik's Reactivity System

// Anti-pattern: plain variable — changes won't trigger re-render
let count = 0

// Correct: useSignal
const count = useSignal(0)

Data Fetching in useVisibleTask$

// Anti-pattern: useVisibleTask$ for data
useVisibleTask$(async () => {
  const data = await fetch('/api/data').then(r => r.json())
  state.value = data
})

// Correct: routeLoader$
export const useData = routeLoader$(async () => {
  return await fetch('/api/data').then(r => r.json())
})

Large Serialized State

// Anti-pattern: serializing entire data set
const allProducts = useSignal(await fetch('/api/products/10000').then(r => r.json()))

// Correct: lazy load, paginate
export const useProducts = routeLoader$(async ({ query }) => {
  const page = Number(query.get('page')) || 1
  return await db.product.findMany({ skip: (page-1)*20, take: 20 })
})

Mixing React and Qwik in Same Component

Don't import or use React hooks inside a Qwik component. The reactivity systems are incompatible.

Over-Serialization

// Anti-pattern: storing non-serializable values in signals
const chartInstance = useSignal<Chart | null>(null) // Chart is a class instance

// Correct: use NoSerialize wrapper
import { NoSerialize } from '@builder.io/qwik'
const chartInstance = useSignal<NoSerialize<Chart>>()

Common Pitfalls

  1. Forgetting $ on event handlers: onClick instead of onClick$ — event never gets lazy loaded.
  2. Using useState from React: Qwik uses useSignal and useStore, not hooks from other frameworks.
  3. Heavy useVisibleTask$ usage: Every useVisibleTask$ blocks resumability. Keep them minimal.
  4. Dynamic $() calls: $(someCondition ? fn1 : fn2) breaks the optimizer. Use static boundaries.
  5. Missing PrefetchServiceWorker: Without it, lazy navigation produces visible network waterfalls.
  6. Mutable state reassignment: state = newValue instead of state.value = newValue for signals.
  7. Nested reactivity with useStore: Objects inside useStore are deeply reactive — use for form state.
  8. Not using typed loaders: routeLoader$ returns unknown without explicit typing.

Best Practices

  1. Every $() should be statically analyzable — no dynamic code inside dollar boundaries.
  2. useSignal for primitives, useStore for objects. Never plain let or const.
  3. Colocate routeLoader$ and routeAction$ in the route file that uses them.
  4. Use useComputed$ for derived values — avoids manual synchronization.
  5. Keep useVisibleTask$ focused on a single concern — one observer per task.
  6. Lazy-load heavy libraries inside $() callbacks, not at module level.
  7. Use NoSerialize wrapper for non-serializable values (class instances, DOM refs).
  8. Prefetch critical routes after initial render with PrefetchServiceWorker.

Compared With

AspectQwikReact (Client)Solid
RenderingResumableHydratingHydrating
Lazy loadingPer-event ($)Per-component (lazy())Per-component (lazy())
ReactivitySignal-based, fine-grainedVirtual DOM diffSignal-based
SerializationAutomatic via QRLManual (JSON)Manual
Server functionsBuilt-in (server$)Via Server ActionsVia SolidStart
Bundle size~10KB (initial)~40KB+ (React runtime)~8KB (initial)

Tooling

  1. npm run qwik build — production build with Qwik optimizer.
  2. npm run qwik dev — dev server with HMR and the Qwik DevTools panel.
  3. npm run qwik lint — ESLint with Qwik-specific rules (ensures $ boundaries).
  4. npm run qwik qwik add — CLI to add integrations (auth, database, UI).
  5. Qwik DevTools browser extension — inspect lazy boundaries and QRL chunks.
  6. @builder.io/qwik-labs — experimental features and utilities.
  7. qwik-speak — internationalization library for Qwik.
  8. qwik-image — optimized images with lazy loading.

Rules

  • No eager code — every component, event, and closure is lazy by default via $.
  • useSignal for primitives, useStore for objects. Never use plain let or const for reactive state.
  • routeLoader$ for data fetching, routeAction$ with <Form> for mutations.
  • useVisibleTask$ only for client-only effects (intersection observers, analytics) — avoid for data fetching.
  • PrefetchServiceWorker is required for instant navigation — without it, lazy loading becomes waterfall.
  • Qwik optimizer must be able to see $() boundaries — no dynamic $ calls.
  • Serialized state stays in HTML — never rely on sessionStorage for critical app state.
  • Dollar boundaries must be statically analyzable — no dynamic expressions inside $().

References

  • references/qwik-city-patterns.md — Qwik City Patterns
  • references/qwik-component-patterns.md — Qwik Component Patterns
  • references/qwik-data.md — Qwik Data Patterns
  • references/qwik-routing.md — Qwik Routing Patterns
  • references/qwik-state-management.md — Qwik State Management
  • references/resumable-patterns.md — Qwik Resumable Patterns
  • references/qwik-component-composition.md — Qwik Component Composition Reference
  • references/qwik-form-validation.md — Qwik Form Validation Reference

Handoff

No artifact produced. Next skill: frontend-universal-testing for Qwik component tests. Or frontend-universal-performance. Carry forward: resumability boundaries, dollar-sign conventions, prefetch strategy.

Skills enregistrées