Getting started
Installation
# Create new Qwik app
npm create qwik@latest
# Or use pnpm
pnpm create qwik@latest
# Start dev server
npm run dev
Project structure
src/
components/ # Reusable components
routes/ # QwikCity file-based routing
index.tsx # Homepage (/)
layout.tsx # Root layout
entry.ssr.tsx # SSR entry point
root.tsx # Root component
Your first component
import { component$ } from '@builder.io/qwik';
export default component$(() => {
return (
<div>
<h1>Hello Qwik!</h1>
</div>
);
});
Resumability
Key concept
Qwik apps resume, they don't hydrate.
// Traditional React/Vue - downloads ALL JavaScript upfront
// ❌ 200KB+ initial bundle, even for static content
// Qwik - downloads ~1KB loader
// ✅ Code loads on interaction, not on page load
// No hydration = instant interactive
How it works:
- Server serializes app state to HTML
- Browser resumes from serialized state
- JavaScript downloads only when needed
- Event handlers load on-demand
The $ suffix
// $ = lazy boundary (code splitting point)
component$(); // Component lazy-loaded
useTask$(); // Task code lazy-loaded
onClick$(); // Handler lazy-loaded
server$(); // Server function
// Without $ = eager (included in bundle)
const value = useSignal(0); // No $ = not lazy
Rule: If function creates lazy boundary, it ends with $
Components
Basic component
import { component$ } from '@builder.io/qwik';
export const Greeting = component$(() => {
return <p>Hello from Qwik</p>;
});
Component with props
import { component$ } from '@builder.io/qwik';
interface ButtonProps {
text: string;
onClick$?: () => void;
}
export const Button = component$<ButtonProps>(
({ text, onClick$ }) => {
return (
<button onClick$={onClick$}>
{text}
</button>
);
}
);
Async components
export const AsyncData = component$(async () => {
// Await data directly in component
const data = await fetch('/api/users')
.then(r => r.json());
return (
<ul>
{data.map((user: any) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
});
State Management
useSignal (reactive primitive)
import { useSignal } from '@builder.io/qwik';
export default component$(() => {
const count = useSignal(0);
return (
<div>
<p>Count: {count.value}</p>
<button
onClick$={() => count.value++}
>
Increment
</button>
</div>
);
});
Always access via .value
useStore (reactive object)
import { useStore } from '@builder.io/qwik';
export default component$(() => {
const state = useStore({
name: 'Alice',
age: 30,
hobbies: ['reading', 'coding']
});
return (
<div>
<input
value={state.name}
onInput$={(e) => {
state.name = e.target.value;
}}
/>
<p>Age: {state.age}</p>
</div>
);
});
No .value needed - direct property access
useComputed$ (derived state)
import { useSignal, useComputed$ } from '@builder.io/qwik';
export default component$(() => {
const firstName = useSignal('John');
const lastName = useSignal('Doe');
// Recomputes when dependencies change
const fullName = useComputed$(() => {
return `${firstName.value} ${lastName.value}`;
});
return <p>{fullName.value}</p>;
});
Lifecycle Hooks
useTask$ (runs on server + client)
import { useTask$, useSignal } from '@builder.io/qwik';
export default component$(() => {
const count = useSignal(0);
// Runs on server (SSR) and when dependencies change
useTask$(({ track }) => {
track(() => count.value); // Track dependency
console.log('Count changed:', count.value);
// Cleanup function (optional)
return () => {
console.log('Cleanup');
};
});
return <button onClick$={() => count.value++}>+</button>;
});
useVisibleTask$ (client-only)
import { useVisibleTask$, useSignal } from '@builder.io/qwik';
export default component$(() => {
const ref = useSignal<Element>();
// Runs ONLY in browser when component becomes visible
useVisibleTask$(() => {
// Access DOM, browser APIs, third-party libraries
const observer = new IntersectionObserver(/* ... */);
observer.observe(ref.value!);
return () => observer.disconnect();
});
return <div ref={ref}>Visible content</div>;
});
⚠️ Use sparingly - breaks resumability
Events
Event handlers
export default component$(() => {
return (
<button
onClick$={() => {
console.log('Clicked!');
}}
>
Click me
</button>
);
});
Event object
export default component$(() => {
return (
<input
onInput$={(event, target) => {
console.log(event.type); // 'input'
console.log(target.value); // Input value
}}
/>
);
});
Event modifiers
export default component$(() => {
return (
<form
// Prevent default behavior
preventdefault:submit
onSubmit$={() => {
console.log('Form submitted');
}}
>
<button type="submit">Submit</button>
</form>
);
});
Modifiers:
| Modifier | Effect |
|---|---|
preventdefault: |
Calls preventDefault() |
stoppropagation: |
Calls stopPropagation() |
Routing (QwikCity)
File-based routing
src/routes/
index.tsx # /
about/
index.tsx # /about
blog/
index.tsx # /blog
[slug]/
index.tsx # /blog/:slug
products/
[category]/
[id]/
index.tsx # /products/:category/:id
Dynamic routes
// src/routes/blog/[slug]/index.tsx
import { component$ } from '@builder.io/qwik';
import { useLocation } from '@builder.io/qwik-city';
export default component$(() => {
const loc = useLocation();
return (
<div>
<h1>Post: {loc.params.slug}</h1>
</div>
);
});
Layout components
// src/routes/layout.tsx
import { component$, Slot } from '@builder.io/qwik';
export default component$(() => {
return (
<div>
<header>My Header</header>
<main>
<Slot /> {/* Child route content */}
</main>
<footer>My Footer</footer>
</div>
);
});
Nested layouts:
routes/
layout.tsx # Root layout
blog/
layout.tsx # Blog layout (inherits root)
index.tsx # Uses both layouts
Navigation
import { component$ } from '@builder.io/qwik';
import { Link, useNavigate } from '@builder.io/qwik-city';
export default component$(() => {
const nav = useNavigate();
return (
<div>
{/* Declarative navigation */}
<Link href="/about">About</Link>
{/* Programmatic navigation */}
<button
onClick$={() => nav('/products')}
>
Go to Products
</button>
</div>
);
});
Data Loading
routeLoader$ (server-side)
// src/routes/products/index.tsx
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
// Runs on server before component renders
export const useProducts = routeLoader$(async () => {
const res = await fetch('https://api.example.com/products');
return res.json();
});
export default component$(() => {
const products = useProducts(); // Access loaded data
return (
<ul>
{products.value.map((p: any) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
});
With route params
// src/routes/user/[id]/index.tsx
import { routeLoader$ } from '@builder.io/qwik-city';
export const useUser = routeLoader$(async (requestEvent) => {
const userId = requestEvent.params.id;
const res = await fetch(`/api/users/${userId}`);
return res.json();
});
export default component$(() => {
const user = useUser();
return <h1>{user.value.name}</h1>;
});
server$ (RPC functions)
import { server$ } from '@builder.io/qwik-city';
// Runs ONLY on server
const saveToDatabase = server$(async function(data: any) {
// Access server-only resources
const db = this.env.get('DATABASE_URL');
await db.save(data);
return { success: true };
});
export default component$(() => {
return (
<button
onClick$={async () => {
// Call from client, executes on server
const result = await saveToDatabase({ name: 'Alice' });
console.log(result);
}}
>
Save
</button>
);
});
⚠️ Never expose secrets in server$ - they run server-side only
Form Actions
routeAction$ (form handling)
import { component$ } from '@builder.io/qwik';
import { routeAction$, Form, zod$, z } from '@builder.io/qwik-city';
// Define validation schema
export const useAddUser = routeAction$(
async (data, requestEvent) => {
// data is validated and type-safe
const user = await createUser(data);
return { success: true, user };
},
// Zod validation
zod$({
name: z.string().min(2),
email: z.string().email(),
})
);
export default component$(() => {
const action = useAddUser();
return (
<Form action={action}>
<input name="name" required />
<input name="email" type="email" required />
<button type="submit">
{action.isRunning ? 'Saving...' : 'Submit'}
</button>
{action.value?.success && (
<p>User created: {action.value.user.name}</p>
)}
</Form>
);
});
Action state
export default component$(() => {
const action = useAddUser();
return (
<div>
{/* Loading state */}
{action.isRunning && <p>Loading...</p>}
{/* Success state */}
{action.value?.success && <p>Success!</p>}
{/* Error state */}
{action.value?.failed && (
<p>Error: {action.value.fieldErrors?.name}</p>
)}
</div>
);
});
Programmatic action
export default component$(() => {
const action = useAddUser();
return (
<button
onClick$={async () => {
// Call action programmatically
const result = await action.submit({
name: 'Bob',
email: 'bob@example.com'
});
console.log(result);
}}
>
Add User
</button>
);
});
Styling
useStylesScoped$ (scoped CSS)
import { component$, useStylesScoped$ } from '@builder.io/qwik';
export default component$(() => {
useStylesScoped$(`
.button {
background: blue;
color: white;
padding: 8px 16px;
}
`);
return <button class="button">Click me</button>;
});
Styles scoped to component only
Global styles
// src/root.tsx
import { useStyles$ } from '@builder.io/qwik';
import globalStyles from './global.css?inline';
export default component$(() => {
useStyles$(globalStyles);
return <Slot />;
});
Tailwind CSS
# Install Tailwind
npm run qwik add tailwind
export default component$(() => {
return (
<button class="bg-blue-500 text-white px-4 py-2">
Tailwind Button
</button>
);
});
CSS Modules
// component.module.css
.button { background: red; }
import styles from './component.module.css';
export default component$(() => {
return <button class={styles.button}>Click</button>;
});
Differences from React
Syntax differences
| Feature | React | Qwik |
|---|---|---|
| Component | function |
component$() |
| State | useState |
useSignal or useStore |
| Effect | useEffect |
useTask$ or useVisibleTask$ |
| Computed | useMemo |
useComputed$ |
| Event handler | onClick={} |
onClick$={} |
| State access | count |
count.value |
| Class | className |
class |
Conceptual differences
// React: Hydration (re-execute all components)
// Downloads full app JavaScript on page load
// Re-runs components to attach event listeners
// Qwik: Resumability (serialize state, lazy load)
// Downloads ~1KB loader on page load
// JavaScript loads only on interaction
// No component re-execution needed
Key insight: Qwik serializes closures and app state to HTML. React rebuilds state from scratch.
State mutations
// React - immutable updates
const [state, setState] = useState({ count: 0 });
setState({ ...state, count: state.count + 1 });
// Qwik - direct mutations
const state = useStore({ count: 0 });
state.count++; // Tracked automatically
No useEffect
// ❌ React pattern
useEffect(() => {
console.log("Count:", count);
}, [count]);
// ✅ Qwik pattern
useTask$(({ track }) => {
track(() => count.value);
console.log("Count:", count.value);
});
Build & Deploy
Build commands
# Development
npm run dev # Dev server (port 5173)
# Production build
npm run build # SSR build
# Preview production
npm run preview # Test SSR build locally
Deployment adapters
# Cloudflare Pages
npm run qwik add cloudflare-pages
# Netlify
npm run qwik add netlify-edge
# Vercel Edge
npm run qwik add vercel-edge
# Node.js
npm run qwik add node-server
# Static site
npm run qwik add static
Environment variables
// .env.local
PUBLIC_API_URL=https://api.example.com
PRIVATE_KEY=secret123
// Access in code
import { component$ } from '@builder.io/qwik';
export default component$(() => {
// Public vars (exposed to browser)
const apiUrl = import.meta.env.PUBLIC_API_URL;
// Private vars (server-only)
// ⚠️ Only use in routeLoader$ or server$
return <div>API: {apiUrl}</div>;
});
⚠️ Prefix with PUBLIC_ for client access
Advanced Patterns
Context (dependency injection)
import { createContextId, useContextProvider, useContext } from '@builder.io/qwik';
// Create context
export const ThemeContext = createContextId<string>('theme');
// Provide value
export const Root = component$(() => {
useContextProvider(ThemeContext, 'dark');
return <Slot />;
});
// Consume value
export const Button = component$(() => {
const theme = useContext(ThemeContext);
return <button class={theme}>Themed</button>;
});
Resource (async state)
import { useResource$, Resource } from '@builder.io/qwik';
export default component$(() => {
const userData = useResource$(async ({ track, cleanup }) => {
// Runs on server and re-runs when dependencies change
const res = await fetch('/api/user');
return res.json();
});
return (
<Resource
value={userData}
onPending={() => <div>Loading...</div>}
onRejected={(error) => <div>Error: {error.message}</div>}
onResolved={(user) => <div>Hello {user.name}</div>}
/>
);
});
Slots (composition)
// Card component
export const Card = component$(() => {
return (
<div class="card">
<div class="header">
<Slot name="header" />
</div>
<div class="body">
<Slot /> {/* Default slot */}
</div>
</div>
);
});
// Usage
<Card>
<h2 q:slot="header">Title</h2>
<p>Card content goes here</p>
</Card>
Dollar ($) reference
| API | Lazy? | When to use |
|---|---|---|
component$() |
Yes | Always for components |
useTask$() |
Yes | Side effects (SSR + client) |
useVisibleTask$() |
Yes | Browser-only side effects |
useComputed$() |
Yes | Derived/computed values |
server$() |
Yes | Server-only functions (RPC) |
routeLoader$() |
Yes | Load data before route renders |
routeAction$() |
Yes | Handle form submissions |
onClick$ |
Yes | Event handlers |
useSignal() |
No | Reactive primitive (single value) |
useStore() |
No | Reactive object (multiple values) |
Common Patterns
Todo list
export default component$(() => {
const todos = useStore<string[]>([]);
const input = useSignal('');
return (
<div>
<input
bind:value={input}
onKeyDown$={(e) => {
if (e.key === 'Enter' && input.value) {
todos.push(input.value);
input.value = '';
}
}}
/>
<ul>
{todos.map((todo, i) => (
<li key={i}>
{todo}
<button onClick$={() => todos.splice(i, 1)}>
Delete
</button>
</li>
))}
</ul>
</div>
);
});
Data fetching with loading
export const useProducts = routeLoader$(async () => {
const res = await fetch('https://api.example.com/products');
return res.json();
});
export default component$(() => {
const products = useProducts();
return (
<Resource
value={products}
onPending={() => <div>Loading products...</div>}
onResolved={(data) => (
<ul>
{data.map((p: any) => <li key={p.id}>{p.name}</li>)}
</ul>
)}
/>
);
});
Protected routes
// src/routes/admin/layout.tsx
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
export const useAuth = routeLoader$(async (requestEvent) => {
const session = requestEvent.cookie.get('session');
if (!session) {
throw requestEvent.redirect(302, '/login');
}
return { user: session };
});
export default component$(() => {
const auth = useAuth();
return <Slot />;
});
Performance Tips
Optimize bundle size
// ✅ Good - lazy load heavy library
const parseMarkdown = server$(async (md: string) => {
const marked = await import("marked");
return marked.parse(md);
});
// ❌ Bad - imports library in component
import { marked } from "marked"; // Adds to bundle
Avoid useVisibleTask$
// ❌ Bad - breaks resumability
useVisibleTask$(() => {
// Browser-only code
window.addEventListener('scroll', handler);
});
// ✅ Good - use QwikCity's built-in features
// Or use declarative event handlers
<div onScroll$={handler}>
Prefetching
import { Link } from '@builder.io/qwik-city';
// Prefetch on hover
<Link href="/products" prefetch="hover">
Products
</Link>
// Prefetch on viewport
<Link href="/about" prefetch="viewport">
About
</Link>
Prefetch strategies:
| Strategy | When |
|---|---|
hover |
User hovers over link |
viewport |
Link enters viewport |
always |
Immediately on page load |
Debugging
Development tools
# Enable Qwik Dev Tools
npm run dev -- --qwikdevtools
# Debug mode (verbose logging)
npm run dev -- --debug
Common errors
Error: Cannot read property 'value' of undefined
// ❌ Forgot .value
const count = useSignal(0);
console.log(count); // Signal object, not value
// ✅ Access .value
console.log(count.value); // 0
Error: Component is not serializable
// ❌ Closure captures non-serializable function
const fn = () => console.log('test');
<button onClick$={() => fn()}>Click</button>
// ✅ Define inline or use $
const fn = $(() => console.log('test'));
<button onClick$={fn}>Click</button>
Inspect serialization
// Check what gets serialized
<button
onClick$={() => {
// This closure must be serializable
// ✅ Primitives, signals, stores
// ❌ Functions, classes, non-serializable objects
}}
>
Click
</button>
Also see
- Qwik Official Docs (qwik.builder.io)
- QwikCity Routing (qwik.builder.io)
- Qwik Examples (qwik.builder.io)
- Qwik GitHub (github.com)
- Resumability Explained (qwik.builder.io)
- Qwik vs React (qwik.builder.io)
- Builder.io (Creators) (builder.io)