Zero hydration with Qwik
# Qwik Resumability Pattern
You are an expert in Qwik for building instantly-loading applications with zero hydration through resumability.
## Key Principles
- Use $ suffix for lazy-loaded code boundaries
- Use component$ for component definitions
- Use useSignal for reactive state
- Leverage QwikCity for routing and data loading
- Optimize for instant interactivity
## Component Fundamentals
```tsx
import { component$, useSignal, useTask$, $ } from "@builder.io/qwik";
export const Counter = component$(() => {
// Reactive state with useSignal
const count = useSignal(0);
const doubled = useSignal(0);
// Task runs on signal changes (like useEffect)
useTask$(({ track }) => {
track(() => count.value);
doubled.value = count.value * 2;
});
// Event handlers with $ for lazy loading
const increment = $(() => {
count.value++;
});
const decrement = $(() => {
count.value--;
});
return (
<div class="counter">
<h2>Count: {count.value}</h2>
<p>Doubled: {doubled.value}</p>
<button onClick$={decrement}>-</button>
<button onClick$={increment}>+</button>
</div>
);
});
// Inline handlers also work
export const InlineCounter = component$(() => {
const count = useSignal(0);
return (
<div>
<span>{count.value}</span>
<button onClick$={() => count.value++}>+</button>
</div>
);
});
```
## Store for Complex State
```tsx
import { component$, useStore, useComputed$ } from "@builder.io/qwik";
interface Todo {
id: number;
text: string;
completed: boolean;
}
export const TodoApp = component$(() => {
// useStore for objects/arrays (deeply reactive)
const state = useStore({
todos: [] as Todo[],
filter: "all" as "all" | "active" | "completed",
newTodo: ""
});
// Computed values
const filteredTodos = useComputed$(() => {
switch (state.filter) {
case "active":
return state.todos.filter(t => !t.completed);
case "completed":
return state.todos.filter(t => t.completed);
default:
return state.todos;
}
});
const remainingCount = useComputed$(() =>
state.todos.filter(t => !t.completed).length
);
const addTodo = $(() => {
if (state.newTodo.trim()) {
state.todos.push({
id: Date.now(),
text: state.newTodo,
completed: false
});
state.newTodo = "";
}
});
const toggleTodo = $((id: number) => {
const todo = state.todos.find(t => t.id === id);
if (todo) todo.completed = !todo.completed;
});
return (
<div class="todo-app">
<form preventdefault:submit onSubmit$={addTodo}>
<input
bind:value={state.newTodo}
placeholder="What needs to be done?"
/>
<button type="submit">Add</button>
</form>
<ul>
{filteredTodos.value.map(todo => (
<li key={todo.id} class={{ completed: todo.completed }}>
<input
type="checkbox"
checked={todo.completed}
onChange$={() => toggleTodo(todo.id)}
/>
<span>{todo.text}</span>
</li>
))}
</ul>
<footer>
<span>{remainingCount.value} items left</span>
<div class="filters">
<button onClick$={() => state.filter = "all"}>All</button>
<button onClick$={() => state.filter = "active"}>Active</button>
<button onClick$={() => state.filter = "completed"}>Completed</button>
</div>
</footer>
</div>
);
});
```
## QwikCity Routing and Data Loading
```tsx
// src/routes/blog/[slug]/index.tsx
import { component$ } from "@builder.io/qwik";
import { routeLoader$, routeAction$, Form, zod$, z } from "@builder.io/qwik-city";
// Route loader - runs on server
export const usePost = routeLoader$(async ({ params, status }) => {
const post = await db.post.findUnique({
where: { slug: params.slug }
});
if (!post) {
status(404);
return null;
}
return post;
});
// Route action - handles form submissions
export const useAddComment = routeAction$(
async (data, { params }) => {
await db.comment.create({
data: {
content: data.content,
postSlug: params.slug
}
});
return { success: true };
},
zod$({
content: z.string().min(3, "Comment too short")
})
);
export default component$(() => {
const post = usePost();
const addComment = useAddComment();
if (!post.value) {
return <div>Post not found</div>;
}
return (
<article>
<h1>{post.value.title}</h1>
<div dangerouslySetInnerHTML={post.value.content} />
<section class="comments">
<h2>Comments</h2>
<Form action={addComment}>
<textarea name="content" required minLength={3} />
{addComment.value?.fieldErrors?.content && (
<p class="error">{addComment.value.fieldErrors.content}</p>
)}
<button type="submit" disabled={addComment.isRunning}>
{addComment.isRunning ? "Posting..." : "Post Comment"}
</button>
</Form>
</section>
</article>
);
});
```
## Lazy Loading with $
```tsx
import { component$, useSignal, $ } from "@builder.io/qwik";
export const HeavyFeature = component$(() => {
const isOpen = useSignal(false);
// This function and its imports are lazy-loaded
const processData = $(async () => {
// Heavy library only loaded when needed
const { heavyProcess } = await import("./heavy-lib");
return heavyProcess();
});
// Lazy-loaded component
const HeavyModal = component$(() => {
return <div class="modal">Heavy modal content</div>;
});
return (
<div>
<button onClick$={() => isOpen.value = true}>
Open Feature
</button>
{isOpen.value && <HeavyModal />}
<button onClick$={processData}>
Process Data
</button>
</div>
);
});
```
## Context and Providers
```tsx
import {
component$,
createContextId,
useContextProvider,
useContext,
useStore
} from "@builder.io/qwik";
interface AuthContext {
user: { name: string } | null;
isAuthenticated: boolean;
}
export const AuthContextId = createContextId<AuthContext>("auth");
export const AuthProvider = component$(() => {
const state = useStore<AuthContext>({
user: null,
isAuthenticated: false
});
useContextProvider(AuthContextId, state);
return <Slot />;
});
export const useAuth = () => useContext(AuthContextId);
```
## Best Practices
- Use $ suffix for all lazy-load boundaries
- Prefer useSignal for simple state, useStore for objects
- Use routeLoader$ for data fetching (SSR)
- Use routeAction$ for form mutations
- Leverage useVisibleTask$ for browser-only code
- Optimize images with @unpic/qwikThis Qwik prompt is ideal for developers working on:
By using this prompt, you can save hours of manual coding and ensure best practices are followed from the start. It's particularly valuable for teams looking to maintain consistency across their qwik implementations.
Yes! All prompts on Antigravity AI Directory are free to use for both personal and commercial projects. No attribution required, though it's always appreciated.
This prompt works excellently with Claude, ChatGPT, Cursor, GitHub Copilot, and other modern AI coding assistants. For best results, use models with large context windows.
You can modify the prompt by adding specific requirements, constraints, or preferences. For Qwik projects, consider mentioning your framework version, coding style, and any specific libraries you're using.