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":[]}}
Die Oberfläche ist lokalisiert; Skill-Beschreibungen und -Dokumente werden per KI automatisch in Ihre Sprache übersetzt. Code, Befehle, Pfade und Fachbegriffe bleiben im Original, maßgeblich ist das Dokument des Autors.
Skills entdecken
Design und Kreativität · 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

Skill-Dokumentation

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.

Gespeicherte Skills