Skip to main content
Advertisement

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

HookPurposeExecution environment
useSignalSimple reactive stateServer + client
useStoreComplex object stateServer + client
useTask$Reactive side effectsServer + client
useVisibleTask$Client-only tasksClient only
useComputed$Computed derived valuesServer + client
useResource$Async data fetchingServer + client
ConceptKey point
SignalRead/write via .value, fine-grained reactivity
SerializationOnly primitives, arrays, and plain objects are allowed
$ rulesMust be created at the component top level only
QRLA 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.

Advertisement