19.3 Core Concepts
This chapter covers Qwik's reactivity system and its core hooks. We'll compare them with React's useState and useEffect to understand the unique concepts that set Qwik apart.
useSignal — Reactive State
What Is a Signal?
A Signal is the fundamental unit of reactive state in Qwik. It's similar to React's useState, but works in a fundamentally different way.
import { component$, useSignal } from '@builder.io/qwik';
export const SignalExample = component$(() => {
// Create a Signal with an initial value
const count = useSignal(0);
const name = useSignal('Alice');
const isVisible = useSignal(false);
// Read a Signal value: access via .value
console.log(count.value); // 0
// Write a Signal value: assign directly to .value
// count.value = 5; ← this works (no setter function needed like useState)
return (
<div>
<p>Count: {count.value}</p>
<p>Name: {name.value}</p>
<button onClick$={() => count.value++}>Increment</button>
<button onClick$={() => name.value = 'Bob'}>Change name</button>
</div>
);
});
Signal vs React useState
// React useState
const [count, setCount] = useState(0);
// read: count
// write: setCount(count + 1) or setCount(prev => prev + 1)
// on change: entire component re-renders
// Qwik useSignal
const count = useSignal(0);
// read: count.value
// write: count.value++ or count.value = newVal
// on change: only the DOM parts that use the Signal update (fine-grained reactivity)
Fine-Grained Reactivity
export const FineGrained = component$(() => {
const a = useSignal(0);
const b = useSignal(0);
// This render function only runs once
// Even when a or b changes, the entire component does NOT re-execute!
console.log('component render (once only)');
return (
<div>
{/* subscribes to a.value: only this text updates when a changes */}
<p>A: {a.value}</p>
{/* subscribes to b.value: only this text updates when b changes */}
<p>B: {b.value}</p>
<button onClick$={() => a.value++}>Increment A</button>
<button onClick$={() => b.value++}>Increment B</button>
</div>
);
});
Signal Types
import type { Signal } from '@builder.io/qwik';
// Type inference
const num = useSignal(0); // Signal<number>
const str = useSignal('hello'); // Signal<string>
const bool = useSignal(false); // Signal<boolean>
// Explicit type annotation
const user = useSignal<User | null>(null);
const list = useSignal<string[]>([]);
// Using the type
function updateUser(sig: Signal<User | null>, data: User) {
sig.value = data;
}
Passing Signals Between Components
// Passing a Signal from parent to child
export const Parent = component$(() => {
const count = useSignal(0);
return (
<div>
<Child count={count} />
<p>Visible in parent: {count.value}</p>
</div>
);
});
interface ChildProps {
count: Signal<number>;
}
export const Child = component$<ChildProps>(({ count }) => {
return (
<div>
<p>Visible in child: {count.value}</p>
<button onClick$={() => count.value++}>
Increment from child
</button>
</div>
);
});
useStore — Complex Object State
Basic useStore Usage
import { component$, useStore } from '@builder.io/qwik';
interface FormState {
name: string;
email: string;
age: number;
errors: {
name?: string;
email?: string;
age?: string;
};
}
export const UserForm = component$(() => {
const form = useStore<FormState>({
name: '',
email: '',
age: 0,
errors: {},
});
const validate = $(() => {
form.errors = {};
if (!form.name) form.errors.name = 'Name is required';
if (!form.email.includes('@')) form.errors.email = 'Enter a valid email';
if (form.age < 0 || form.age > 150) form.errors.age = 'Enter a valid age';
return Object.keys(form.errors).length === 0;
});
return (
<form preventdefault:submit onSubmit$={async () => {
if (await validate()) {
console.log('Form submitted:', form.name, form.email);
}
}}>
<div>
<input
value={form.name}
onInput$={(e) => form.name = (e.target as HTMLInputElement).value}
placeholder="Name"
/>
{form.errors.name && <span class="error">{form.errors.name}</span>}
</div>
<div>
<input
type="email"
value={form.email}
onInput$={(e) => form.email = (e.target as HTMLInputElement).value}
placeholder="Email"
/>
{form.errors.email && <span class="error">{form.errors.email}</span>}
</div>
<button type="submit">Submit</button>
</form>
);
});
Deep Reactivity
export const DeepReactivity = component$(() => {
const state = useStore({
user: {
profile: {
name: 'Alice',
address: {
city: 'New York',
zip: '10001'
}
}
},
items: [
{ id: 1, name: 'Apple', done: false },
{ id: 2, name: 'Banana', done: true },
]
});
return (
<div>
{/* nested objects can be mutated directly */}
<p>{state.user.profile.address.city}</p>
<button onClick$={() => {
state.user.profile.address.city = 'Los Angeles'; // reactivity preserved at any depth
}}>
Change city
</button>
{/* array mutations also maintain reactivity */}
{state.items.map(item => (
<div key={item.id}>
<input
type="checkbox"
checked={item.done}
onChange$={() => item.done = !item.done} // mutate array item directly
/>
{item.name}
</div>
))}
<button onClick$={() => {
state.items.push({ id: Date.now(), name: 'New item', done: false });
}}>
Add item
</button>
</div>
);
});
Choosing Between useStore and useSignal
// useSignal: for simple primitives, or when an array/object is replaced entirely
const count = useSignal(0);
const userList = useSignal<User[]>([]); // when the whole array is replaced
// useStore: when updating specific fields of a complex object
const user = useStore({
name: 'Alice',
settings: { theme: 'dark' }
});
user.settings.theme = 'light'; // mutating inside an object → useStore is better
useTask$ — Side Effects
Basic Usage
import { component$, useSignal, useTask$ } from '@builder.io/qwik';
import { isServer } from '@builder.io/qwik/build';
export const TaskExample = component$(() => {
const count = useSignal(0);
const doubled = useSignal(0);
// useTask$ runs on both server and client
useTask$(({ track }) => {
// register reactivity with track()
const current = track(() => count.value);
// runs every time count.value changes
doubled.value = current * 2;
console.log(`count: ${current}, doubled: ${doubled.value}`);
});
return (
<div>
<p>Count: {count.value}</p>
<p>Doubled: {doubled.value}</p>
<button onClick$={() => count.value++}>Increment</button>
</div>
);
});
Server/Client Execution Branching
import { useTask$ } from '@builder.io/qwik';
import { isServer, isBrowser } from '@builder.io/qwik/build';
export const ServerClientTask = component$(() => {
const data = useSignal('');
useTask$(async () => {
if (isServer) {
// code that runs only on the server
// window and document are not available
// Node.js APIs and process.env are available
data.value = 'Data fetched on server';
console.log('Running on server');
}
if (isBrowser) {
// code that runs only in the browser
data.value = localStorage.getItem('cached') || 'default value';
console.log('Running in browser');
}
});
return <p>{data.value}</p>;
});
Tracking Multiple Signals with track()
export const MultiTrack = component$(() => {
const firstName = useSignal('Jane');
const lastName = useSignal('Smith');
const fullName = useSignal('');
useTask$(({ track }) => {
// track both Signals
const first = track(() => firstName.value);
const last = track(() => lastName.value);
fullName.value = `${first} ${last}`;
});
return (
<div>
<input
value={firstName.value}
onInput$={(e) => firstName.value = (e.target as HTMLInputElement).value}
/>
<input
value={lastName.value}
onInput$={(e) => lastName.value = (e.target as HTMLInputElement).value}
/>
<p>Full name: {fullName.value}</p>
</div>
);
});
cleanup Function (Resource Cleanup)
export const CleanupExample = component$(() => {
const active = useSignal(false);
const status = useSignal('Idle');
useTask$(({ track, cleanup }) => {
const isActive = track(() => active.value);
if (!isActive) return;
// start a timer or subscription
const timer = setInterval(() => {
status.value = `Active (${new Date().toLocaleTimeString()})`;
}, 1000);
// cleanup: called before the next run or when the component is removed
cleanup(() => {
clearInterval(timer);
status.value = 'Stopped';
});
});
return (
<div>
<p>{status.value}</p>
<button onClick$={() => active.value = !active.value}>
{active.value ? 'Stop' : 'Start'}
</button>
</div>
);
});
useVisibleTask$ — Client-Only Tasks
Basic Usage
import { component$, useSignal, useVisibleTask$ } from '@builder.io/qwik';
export const ClientOnlyTask = component$(() => {
const windowWidth = useSignal(0);
const scrollY = useSignal(0);
// useVisibleTask$ runs on the client when the component enters the viewport
useVisibleTask$(() => {
// window and document are safely accessible here
windowWidth.value = window.innerWidth;
scrollY.value = window.scrollY;
const handleResize = () => windowWidth.value = window.innerWidth;
const handleScroll = () => scrollY.value = window.scrollY;
window.addEventListener('resize', handleResize);
window.addEventListener('scroll', handleScroll);
// cleanup
return () => {
window.removeEventListener('resize', handleResize);
window.removeEventListener('scroll', handleScroll);
};
});
return (
<div>
<p>Window width: {windowWidth.value}px</p>
<p>Scroll position: {scrollY.value}px</p>
</div>
);
});
The eagerness Option
// control when useVisibleTask$ runs
useVisibleTask$(() => {
// default: runs when the component enters the viewport
}, { strategy: 'intersection-observer' }); // default
useVisibleTask$(() => {
// runs immediately after the document is fully loaded
}, { strategy: 'document-ready' });
useVisibleTask$(() => {
// runs when the browser is idle (requestIdleCallback)
}, { strategy: 'document-idle' });
Real-World Example: Initializing a Chart Library
import { component$, useSignal, useVisibleTask$ } from '@builder.io/qwik';
export const ChartComponent = component$(() => {
const chartRef = useSignal<HTMLCanvasElement>();
const data = useSignal([10, 20, 15, 30, 25]);
useVisibleTask$(({ track }) => {
const canvas = chartRef.value;
if (!canvas) return;
// initialize a client-only library like Chart.js
// (useVisibleTask$ is needed because window/canvas don't exist on the server)
const chart = new (window as any).Chart(canvas, {
type: 'bar',
data: {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May'],
datasets: [{
data: data.value,
backgroundColor: 'rgba(99, 102, 241, 0.5)'
}]
}
});
return () => chart.destroy(); // cleanup
});
return (
<div class="chart-container">
<canvas ref={chartRef} width="400" height="300" />
</div>
);
});
useComputed$ — Derived Values
Basic Usage
import { component$, useSignal, useComputed$ } from '@builder.io/qwik';
export const ComputedExample = component$(() => {
const items = useSignal([
{ id: 1, name: 'Apple', price: 1.5, qty: 3 },
{ id: 2, name: 'Banana', price: 0.75, qty: 5 },
{ id: 3, name: 'Grapes', price: 3.0, qty: 2 },
]);
const taxRate = useSignal(0.1);
// useComputed$: automatically recomputes when dependent Signals change
const subtotal = useComputed$(() =>
items.value.reduce((sum, item) => sum + item.price * item.qty, 0)
);
const tax = useComputed$(() => subtotal.value * taxRate.value);
const total = useComputed$(() => subtotal.value + tax.value);
const formattedTotal = useComputed$(() =>
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(total.value)
);
return (
<div>
<ul>
{items.value.map(item => (
<li key={item.id}>
{item.name}: ${item.price} × {item.qty}
</li>
))}
</ul>
<div class="summary">
<p>Subtotal: ${subtotal.value.toFixed(2)}</p>
<p>Tax ({(taxRate.value * 100).toFixed(0)}%): ${tax.value.toFixed(2)}</p>
<p><strong>Total: {formattedTotal.value}</strong></p>
</div>
<label>
Tax rate:
<input
type="range" min="0" max="0.3" step="0.01"
value={taxRate.value}
onInput$={(e) => taxRate.value = parseFloat((e.target as HTMLInputElement).value)}
/>
{(taxRate.value * 100).toFixed(0)}%
</label>
</div>
);
});
Async Computation (useComputed$ with async)
// useComputed$ also supports async
const processedData = useComputed$(async () => {
const raw = rawData.value;
if (!raw.length) return [];
// async processing
const processed = await processData(raw);
return processed;
});
useResource$ — Async Data Fetching
Basic Usage
import {
component$,
useSignal,
useResource$,
Resource
} from '@builder.io/qwik';
interface Post {
id: number;
title: string;
body: string;
userId: number;
}
export const PostList = component$(() => {
const page = useSignal(1);
// useResource$: async data source that reacts to Signals
const postsResource = useResource$<Post[]>(async ({ track, cleanup }) => {
// track the page Signal
const currentPage = track(() => page.value);
// cancel previous request with AbortController
const controller = new AbortController();
cleanup(() => controller.abort());
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts?_page=${currentPage}&_limit=5`,
{ signal: controller.signal }
);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
});
return (
<div>
{/* Resource component handles loading/error/success states */}
<Resource
value={postsResource}
onPending={() => <div>Loading...</div>}
onRejected={(error) => <div>Error: {error.message}</div>}
onResolved={(posts) => (
<ul>
{posts.map(post => (
<li key={post.id}>
<h3>{post.title}</h3>
<p>{post.body.substring(0, 100)}...</p>
</li>
))}
</ul>
)}
/>
<div class="pagination">
<button
onClick$={() => page.value--}
disabled={page.value <= 1}
>
Previous
</button>
<span>Page {page.value}</span>
<button onClick$={() => page.value++}>Next</button>
</div>
</div>
);
});
Combining useResource$ with useSignal (Search Example)
export const SearchPage = component$(() => {
const query = useSignal('');
const debouncedQuery = useSignal('');
// debounce
useTask$(({ track, cleanup }) => {
track(() => query.value);
const timer = setTimeout(() => {
debouncedQuery.value = query.value;
}, 300);
cleanup(() => clearTimeout(timer));
});
const results = useResource$<SearchResult[]>(async ({ track, cleanup }) => {
const q = track(() => debouncedQuery.value);
if (!q || q.length < 2) return [];
const controller = new AbortController();
cleanup(() => controller.abort());
const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`, {
signal: controller.signal
});
return res.json();
});
return (
<div>
<input
type="search"
value={query.value}
onInput$={(e) => query.value = (e.target as HTMLInputElement).value}
placeholder="Type at least 2 characters to search"
/>
<Resource
value={results}
onPending={() => <p>Searching...</p>}
onRejected={(err) => <p>Search error: {err.message}</p>}
onResolved={(items) =>
items.length === 0
? <p>No results found</p>
: <ul>{items.map(item => <li key={item.id}>{item.title}</li>)}</ul>
}
/>
</div>
);
});
Serializability — Understanding QRL
Why Serialization Matters
Qwik's core idea is serializing server state into HTML. For this to work, all state must be serializable.
// Serializable types
const signal1 = useSignal(42); // number ✓
const signal2 = useSignal('hello'); // string ✓
const signal3 = useSignal(true); // boolean ✓
const signal4 = useSignal([1, 2, 3]); // array ✓
const signal5 = useSignal({ a: 1 }); // plain object ✓
const signal6 = useSignal(null); // null ✓
// Non-serializable types
const bad1 = useSignal(() => {}); // function ✗ (error)
const bad2 = useSignal(new Map()); // Map ✗
const bad3 = useSignal(new Set()); // Set ✗
const bad4 = useSignal(document.body); // DOM element ✗
const bad5 = useSignal(new MyClass()); // class instance ✗ (use with care)
Excluding Values from Serialization with noSerialize()
import { component$, useStore, noSerialize } from '@builder.io/qwik';
export const NonSerializable = component$(() => {
const state = useStore({
name: 'Alice',
// marked with noSerialize: not serialized into HTML (client-only)
thirdPartyLib: noSerialize(null as any),
});
useVisibleTask$(() => {
// initialize on the client only
state.thirdPartyLib = noSerialize(new SomeHeavyLibrary());
});
return <div>{state.name}</div>;
});
Understanding QRL (Qwik URL Reference)
// QRL is a reference to a lazily loaded function
// Using the $ sign makes the Optimizer generate QRLs automatically
import type { QRL } from '@builder.io/qwik';
import { $ } from '@builder.io/qwik';
// Explicit QRL creation
const myHandler: QRL<() => void> = $(() => {
console.log('handler executed');
});
// Dynamic QRL inside a component
export const DynamicQRL = component$(() => {
const action = useSignal<'greet' | 'farewell'>('greet');
// conditional handler (branching inside $)
const handleClick = $(() => {
if (action.value === 'greet') {
alert('Hello!');
} else {
alert('Goodbye!');
}
});
return (
<div>
<select
value={action.value}
onChange$={(e) => action.value = (e.target as HTMLSelectElement).value as any}
>
<option value="greet">Greet</option>
<option value="farewell">Farewell</option>
</select>
<button onClick$={handleClick}>Execute</button>
</div>
);
});
$ Sign Rules — What the Optimizer Does
$ Sign Placement Rules
// Rule 1: use at the component top level or module level
export const MyComponent = component$(() => { // ✓ module level
const handler = $(() => { ... }); // ✓ component top level
return <button onClick$={handler}>Click</button>;
});
// Rule 2: do not create $ functions inside conditionals
export const Bad = component$(() => {
const show = useSignal(true);
// ✗ don't do this
// if (show.value) {
// const handler = $(() => { ... }); // cannot create $ inside a conditional
// }
// ✓ correct approach: always create at the top level
const handler = $(() => {
if (show.value) console.log('visible');
});
return <button onClick$={handler}>Click</button>;
});
// Rule 3: do not create $ functions inside loops
export const BadLoop = component$(() => {
const items = [1, 2, 3];
// ✗ don't do this
// const handlers = items.map(i => $(() => console.log(i)));
// ✓ correct approach: pass data through the event
return (
<ul>
{items.map(item => (
<li key={item}>
<button onClick$={() => console.log(item)}>{item}</button>
</li>
))}
</ul>
);
});
Types of $ Signs and Their Roles
// all $ patterns
import {
component$, // component definition
useTask$, // task/effect
useVisibleTask$, // client task
useComputed$, // computed value
$, // lazy-load any function
sync$, // synchronous QRL (special cases)
} from '@builder.io/qwik';
import {
routeLoader$, // server data loader
routeAction$, // form action
server$, // server-only function
globalAction$, // global action
} from '@builder.io/qwik-city';
Practical Examples
Example 1: Real-Time Search Filter
export const FilterList = component$(() => {
const searchQuery = useSignal('');
const allItems = useSignal([
'Apple', 'Banana', 'Grapes', 'Strawberry', 'Kiwi',
'Mango', 'Pineapple', 'Watermelon', 'Peach', 'Melon'
]);
const filteredItems = useComputed$(() => {
const q = searchQuery.value.toLowerCase();
return allItems.value.filter(item => item.toLowerCase().includes(q));
});
return (
<div>
<input
type="search"
value={searchQuery.value}
onInput$={(e) => searchQuery.value = (e.target as HTMLInputElement).value}
placeholder="Search fruits..."
/>
<p>{filteredItems.value.length} results</p>
<ul>
{filteredItems.value.map(item => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
});
Example 2: Auto-Save Form
export const AutoSaveForm = component$(() => {
const content = useSignal('');
const saveStatus = useSignal<'idle' | 'saving' | 'saved'>('idle');
// auto-save: save 3 seconds after content changes
useTask$(({ track, cleanup }) => {
const text = track(() => content.value);
if (!text) return;
const timer = setTimeout(async () => {
saveStatus.value = 'saving';
await new Promise(resolve => setTimeout(resolve, 500)); // simulate server request
localStorage.setItem('draft', text);
saveStatus.value = 'saved';
// reset to idle after 3 seconds
setTimeout(() => saveStatus.value = 'idle', 3000);
}, 3000);
cleanup(() => clearTimeout(timer));
});
// restore saved content on initial load
useVisibleTask$(() => {
const saved = localStorage.getItem('draft');
if (saved) content.value = saved;
});
return (
<div>
<textarea
value={content.value}
onInput$={(e) => content.value = (e.target as HTMLTextAreaElement).value}
rows={10}
cols={50}
placeholder="Auto-saves as you type..."
/>
<div class="save-status">
{saveStatus.value === 'saving' && 'Saving...'}
{saveStatus.value === 'saved' && 'Saved ✓'}
{saveStatus.value === 'idle' && content.value && '(auto-save pending)'}
</div>
</div>
);
});
Example 3: Async Data with Error Handling
interface GitHubUser {
login: string;
name: string;
avatar_url: string;
public_repos: number;
followers: number;
}
export const GitHubProfile = component$(() => {
const username = useSignal('');
const searchInput = useSignal('');
const userResource = useResource$<GitHubUser>(async ({ track, cleanup }) => {
const name = track(() => username.value);
if (!name) return null as any;
const controller = new AbortController();
cleanup(() => controller.abort());
const res = await fetch(`https://api.github.com/users/${name}`, {
signal: controller.signal
});
if (res.status === 404) throw new Error('User not found');
if (!res.ok) throw new Error(`API error: ${res.status}`);
return res.json();
});
return (
<div class="github-profile">
<div class="search-bar">
<input
type="text"
value={searchInput.value}
onInput$={(e) => searchInput.value = (e.target as HTMLInputElement).value}
placeholder="GitHub username"
onKeyDown$={(e) => {
if (e.key === 'Enter') username.value = searchInput.value;
}}
/>
<button onClick$={() => username.value = searchInput.value}>
Search
</button>
</div>
{username.value && (
<Resource
value={userResource}
onPending={() => (
<div class="loading">
<span>Loading profile...</span>
</div>
)}
onRejected={(error) => (
<div class="error">
<p>Error: {error.message}</p>
<button onClick$={() => username.value = ''}>Try again</button>
</div>
)}
onResolved={(user) =>
user ? (
<div class="profile-card">
<img src={user.avatar_url} alt={user.login} width={80} height={80} />
<h2>{user.name || user.login}</h2>
<p>@{user.login}</p>
<div class="stats">
<span>Repos: {user.public_repos}</span>
<span>Followers: {user.followers}</span>
</div>
</div>
) : <p>Enter a username to search</p>
}
/>
)}
</div>
);
});
Pro Tips
Tip 1: Array of Signals vs Signal of Array
// Array of Signals: each item reacts independently
// → only the changed item updates
const items = [useSignal('first'), useSignal('second')];
// Signal of array: the entire array is one Signal
// → any change in the array causes the whole list to re-render
const itemList = useSignal(['first', 'second']);
// or
const itemStore = useStore({ items: ['first', 'second'] });
// itemStore.items[0] = 'changed' → only that part updates
// In practice, the deep reactivity of useStore is most useful
Tip 2: Choosing Between useTask$ and useVisibleTask$
useTask$ → runs on both server and client
→ use for data preparation during SSR, reactive calculations
→ cannot use window or document
useVisibleTask$ → client only
→ DOM manipulation, browser APIs, third-party library initialization
→ window and document are available
→ runs when the component enters the viewport (default)
Tip 3: Preventing Memory Leaks
export const EventListener = component$(() => {
useVisibleTask$(({ cleanup }) => {
const handler = (e: MouseEvent) => console.log(e.clientX);
document.addEventListener('mousemove', handler);
// always remove event listeners in cleanup!
cleanup(() => document.removeEventListener('mousemove', handler));
});
return <div>Tracking mouse position</div>;
});
Tip 4: Custom Hook Pattern
// src/hooks/use-local-storage.ts
import { useSignal, useVisibleTask$ } from '@builder.io/qwik';
import type { Signal } from '@builder.io/qwik';
// Custom hook: a Signal synchronized with localStorage
export function useLocalStorage<T>(key: string, initialValue: T): Signal<T> {
const value = useSignal<T>(initialValue);
useVisibleTask$(({ track }) => {
// initial load: read from localStorage
const stored = localStorage.getItem(key);
if (stored) {
try {
value.value = JSON.parse(stored);
} catch {
value.value = initialValue;
}
}
});
useVisibleTask$(({ track }) => {
// save to localStorage on value change
const current = track(() => value.value);
localStorage.setItem(key, JSON.stringify(current));
});
return value;
}
// Usage
export const Settings = component$(() => {
const theme = useLocalStorage('theme', 'light');
return (
<button onClick$={() => theme.value = theme.value === 'light' ? 'dark' : 'light'}>
Current theme: {theme.value}
</button>
);
});
Tip 5: useResource$ Retry Pattern
export const RetryableResource = component$(() => {
const retryCount = useSignal(0);
const data = useResource$<any>(async ({ track }) => {
track(() => retryCount.value); // retry trigger
const res = await fetch('/api/unstable-endpoint');
if (!res.ok) throw new Error('Request failed');
return res.json();
});
return (
<Resource
value={data}
onPending={() => <p>Loading...</p>}
onRejected={(err) => (
<div>
<p>Error: {err.message}</p>
<button onClick$={() => retryCount.value++}>
Retry ({retryCount.value} attempts)
</button>
</div>
)}
onResolved={(result) => <pre>{JSON.stringify(result, null, 2)}</pre>}
/>
);
});
Summary
| Hook | Purpose | Execution environment |
|---|---|---|
useSignal | Simple reactive state | Server + client |
useStore | Complex object state | Server + client |
useTask$ | Reactive side effects | Server + client |
useVisibleTask$ | Client-only tasks | Client only |
useComputed$ | Computed derived values | Server + client |
useResource$ | Async data fetching | Server + client |
| Concept | Key point |
|---|---|
| Signal | Read/write via .value, fine-grained reactivity |
| Serialization | Only primitives, arrays, and plain objects are allowed |
| $ rules | Must be created at the component top level only |
| QRL | A reference to a lazily loaded function |
| track() | Registers a Signal subscription inside useTask$ |
| cleanup() | Registers a resource cleanup function |
You now understand Qwik's core reactivity system. The next chapter covers QwikCity routing, server-side features, and API handling.