Fix: Qwik Not Working — Components Not Rendering, useSignal Not Reactive, or Serialization Errors
Quick Answer
How to fix Qwik issues — component$ boundaries, useSignal and useStore reactivity, serialization with dollar signs, useTask$ and useVisibleTask$, Qwik City routing, and integration with React components.
The Problem
A Qwik component renders on the server but doesn’t become interactive in the browser:
import { component$, useSignal } from '@builder.io/qwik';
export default component$(() => {
const count = useSignal(0);
return (
<button onClick$={() => count.value++}>
Count: {count.value}
</button>
);
});
// Button renders but clicking does nothingOr a serialization error appears at build time:
Error: Qwik Serialization Error: Cannot serialize class instance
Offending value: [object Date]Or data fetching runs twice — once on the server and once on the client:
export const useUserData = routeLoader$(() => {
console.log('Fetching user'); // Prints twice
return fetch('/api/user').then(r => r.json());
});Or a third-party library doesn’t work because it accesses window during SSR:
ReferenceError: window is not definedWhy This Happens
Qwik uses resumability instead of hydration. The server renders the HTML and serializes the component state into the DOM. The browser doesn’t re-execute component code until user interaction triggers it. This architecture creates specific constraints:
- The
$suffix marks serialization boundaries —component$(),onClick$(),useTask$(), and other$-suffixed APIs tell the Qwik optimizer where to split code. Anything crossing a$boundary must be serializable to JSON. Classes, DOM nodes, functions (without$), and closures over non-serializable values break this contract. - State must use
useSignaloruseStore— plain JavaScript variables aren’t tracked.let count = 0inside a component doesn’t trigger re-renders when mutated. Qwik’s reactivity system only tracks.valuereads on signals and property access on stores. routeLoader$runs only on the server — it executes during SSR and its result is serialized into the HTML. It doesn’t run again on the client unless a navigation triggers it. If you see it running “twice,” it’s because a full page load (server) is followed by client-side code that also runs something.- Browser APIs aren’t available during SSR —
window,document,localStorage, and other browser globals don’t exist on the server. UseuseVisibleTask$()for code that must run in the browser.
Fix 1: Component and Reactivity Basics
npm create qwik@latestimport { component$, useSignal, useStore, $ } from '@builder.io/qwik';
// component$ — marks a component boundary
export const Counter = component$(() => {
// useSignal — for primitive values
const count = useSignal(0);
// useStore — for objects (deeply reactive)
const state = useStore({
items: ['Apple', 'Banana'],
filter: '',
});
// $() wraps functions that cross serialization boundaries
const increment = $(() => {
count.value++;
});
const addItem = $((item: string) => {
state.items.push(item);
});
return (
<div>
{/* Access signal with .value */}
<button onClick$={increment}>Count: {count.value}</button>
{/* Inline handlers also use $() syntax via onClick$ */}
<button onClick$={() => count.value--}>Decrement</button>
{/* Store properties are reactive */}
<input
value={state.filter}
onInput$={(e: Event) => {
state.filter = (e.target as HTMLInputElement).value;
}}
/>
<ul>
{state.items
.filter(item => item.toLowerCase().includes(state.filter.toLowerCase()))
.map((item, i) => (
<li key={i}>{item}</li>
))}
</ul>
<button onClick$={() => addItem('Cherry')}>Add Cherry</button>
</div>
);
});Common reactivity mistakes:
// WRONG — destructuring loses reactivity
const { value } = count; // value is a static number, not reactive
<span>{value}</span> // Never updates
// CORRECT — access .value in JSX
<span>{count.value}</span> // Updates when count changes
// WRONG — plain variable isn't tracked
let name = 'Alice';
<button onClick$={() => { name = 'Bob'; }}>Change</button>
// Button works but UI doesn't update
// CORRECT — use useSignal
const name = useSignal('Alice');
<button onClick$={() => { name.value = 'Bob'; }}>Change</button>
// WRONG — replacing store object
const state = useStore({ items: [] });
state = { items: ['new'] }; // ERROR — can't reassign store
// CORRECT — mutate store properties
state.items = ['new']; // Replace array
state.items.push('new item'); // Mutate arrayFix 2: Fix Serialization Errors
Everything that crosses a $ boundary must be serializable:
// WRONG — Date is not serializable by default
const state = useStore({
createdAt: new Date(), // Serialization error
});
// CORRECT — store dates as strings or timestamps
const state = useStore({
createdAt: new Date().toISOString(), // String is serializable
});
// CORRECT — use noSerialize for non-serializable values
import { noSerialize, type NoSerialize } from '@builder.io/qwik';
const state = useStore<{ chart: NoSerialize<ChartInstance> | undefined }>({
chart: undefined,
});
// Set non-serializable values inside useVisibleTask$ (runs only in browser)
useVisibleTask$(() => {
const chart = new ChartLibrary('#chart');
state.chart = noSerialize(chart);
// Cleanup
return () => chart.destroy();
});
// WRONG — passing a class instance through a $() boundary
class UserService {
getUser() { return fetch('/api/user'); }
}
const service = new UserService();
const handler = $(() => service.getUser()); // Serialization error
// CORRECT — use plain functions or server$
const getUser = server$(async () => {
return fetch('/api/user').then(r => r.json());
});
const handler = $(() => getUser());Fix 3: Data Loading with routeLoader$ and server$
// src/routes/posts/index.tsx — Qwik City route
import { component$ } from '@builder.io/qwik';
import { routeLoader$, routeAction$, Form, z, zod$ } from '@builder.io/qwik-city';
// routeLoader$ — runs on the server during page load
export const usePostsLoader = routeLoader$(async (requestEvent) => {
const response = await fetch('https://api.example.com/posts', {
headers: {
Authorization: `Bearer ${requestEvent.env.get('API_KEY')}`,
},
});
if (!response.ok) {
throw requestEvent.redirect(302, '/error');
}
return response.json() as Promise<Post[]>;
});
// routeAction$ — server-side mutation triggered by form submission
export const useCreatePost = routeAction$(
async (data, requestEvent) => {
const response = await fetch('https://api.example.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!response.ok) {
return requestEvent.fail(400, { message: 'Failed to create post' });
}
return { success: true };
},
// Validate with Zod
zod$({
title: z.string().min(1).max(200),
body: z.string().min(10),
}),
);
export default component$(() => {
const posts = usePostsLoader(); // Access loader data
const createAction = useCreatePost();
return (
<div>
{/* Loader data */}
<ul>
{posts.value.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
{/* Form triggers routeAction$ */}
<Form action={createAction}>
<input name="title" required />
<textarea name="body" required />
{createAction.value?.failed && (
<p>{createAction.value.message}</p>
)}
<button type="submit" disabled={createAction.isRunning}>
{createAction.isRunning ? 'Creating...' : 'Create Post'}
</button>
</Form>
</div>
);
});// server$ — RPC-style server function callable from the client
import { server$ } from '@builder.io/qwik-city';
const searchUsers = server$(async function (query: string) {
// This code runs on the server — access env, DB, etc.
const db = this.env.get('DATABASE_URL');
const results = await dbQuery(`SELECT * FROM users WHERE name LIKE $1`, [`%${query}%`]);
return results;
});
// Call from a component — transparently makes a server request
export const SearchComponent = component$(() => {
const results = useSignal<User[]>([]);
return (
<div>
<input
onInput$={async (e: Event) => {
const query = (e.target as HTMLInputElement).value;
if (query.length > 2) {
results.value = await searchUsers(query);
}
}}
/>
<ul>
{results.value.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
});Fix 4: useTask$ vs useVisibleTask$
Two task types serve different purposes:
import { component$, useSignal, useTask$, useVisibleTask$ } from '@builder.io/qwik';
export default component$(() => {
const data = useSignal<string>('');
const windowWidth = useSignal(0);
// useTask$ — runs on server AND client
// Tracks signal dependencies and re-runs when they change
useTask$(({ track }) => {
// Track a specific signal
const value = track(() => data.value);
console.log('Data changed to:', value);
// This runs on the server during SSR
// and on the client when data.value changes
});
// useTask$ with cleanup
useTask$(({ track, cleanup }) => {
const query = track(() => data.value);
// Debounce — cleanup cancels the previous timer
const timer = setTimeout(() => {
// Fetch search results
}, 300);
cleanup(() => clearTimeout(timer));
});
// useVisibleTask$ — runs ONLY in the browser
// Use for: DOM manipulation, browser APIs, third-party libraries
useVisibleTask$(() => {
// Safe to use window, document, localStorage
windowWidth.value = window.innerWidth;
const handleResize = () => {
windowWidth.value = window.innerWidth;
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
});
// useVisibleTask$ with strategy
useVisibleTask$(
() => {
// Initialize a chart library
const chart = new Chart('#chart', { data: [] });
return () => chart.destroy();
},
{
strategy: 'intersection-observer', // Run when element is visible
// strategy: 'document-ready', // Run on DOMContentLoaded
// strategy: 'document-idle', // Run when browser is idle (default)
},
);
return (
<div>
<input
value={data.value}
onInput$={(e: Event) => {
data.value = (e.target as HTMLInputElement).value;
}}
/>
<p>Window width: {windowWidth.value}</p>
</div>
);
});Fix 5: Qwik City Routing
src/routes/
├── index.tsx # /
├── about/
│ └── index.tsx # /about
├── posts/
│ ├── index.tsx # /posts
│ ├── [postId]/
│ │ └── index.tsx # /posts/:postId
│ └── layout.tsx # Shared layout for /posts/*
├── layout.tsx # Root layout
└── 404.tsx # Custom 404 page// src/routes/layout.tsx — root layout
import { component$, Slot } from '@builder.io/qwik';
import { Link, useLocation } from '@builder.io/qwik-city';
export default component$(() => {
const loc = useLocation();
return (
<>
<nav>
<Link href="/" class={{ active: loc.url.pathname === '/' }}>
Home
</Link>
<Link href="/posts/" class={{ active: loc.url.pathname.startsWith('/posts') }}>
Posts
</Link>
</nav>
<main>
<Slot /> {/* Child route renders here */}
</main>
</>
);
});
// src/routes/posts/[postId]/index.tsx — dynamic route
import { component$ } from '@builder.io/qwik';
import { routeLoader$, useLocation } from '@builder.io/qwik-city';
export const usePost = routeLoader$(async (requestEvent) => {
const postId = requestEvent.params.postId;
const res = await fetch(`https://api.example.com/posts/${postId}`);
if (!res.ok) throw requestEvent.redirect(302, '/posts/');
return res.json();
});
export default component$(() => {
const post = usePost();
return (
<article>
<h1>{post.value.title}</h1>
<p>{post.value.body}</p>
</article>
);
});Fix 6: Integrate Third-Party Libraries
Non-Qwik libraries need special handling for the $ boundary:
import { component$, useSignal, useVisibleTask$, noSerialize, type NoSerialize } from '@builder.io/qwik';
// Example: Leaflet map
export const MapComponent = component$<{ lat: number; lng: number }>(({ lat, lng }) => {
const mapRef = useSignal<HTMLDivElement>();
const mapInstance = useSignal<NoSerialize<L.Map>>();
useVisibleTask$(async () => {
// Dynamic import — loads only in browser
const L = await import('leaflet');
await import('leaflet/dist/leaflet.css');
if (!mapRef.value) return;
const map = L.map(mapRef.value).setView([lat, lng], 13);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
L.marker([lat, lng]).addTo(map);
mapInstance.value = noSerialize(map);
return () => map.remove();
});
return <div ref={mapRef} style={{ height: '400px', width: '100%' }} />;
});
// Example: Using a React component in Qwik
// Install: npm install @builder.io/qwik-react react react-dom
import { qwikify$ } from '@builder.io/qwik-react';
import { DatePicker } from 'some-react-datepicker';
// Wrap React component for Qwik
const QwikDatePicker = qwikify$(DatePicker, { eagerness: 'hover' });
export const FormWithDatePicker = component$(() => {
const date = useSignal('');
return (
<QwikDatePicker
selected={date.value}
onChange$={(newDate: string) => {
date.value = newDate;
}}
/>
);
});Still Not Working?
Component renders but buttons/inputs are not interactive — Qwik lazy-loads event handlers. If the JavaScript chunk fails to load (network error, incorrect base URL), interactions silently fail. Check the browser Network tab for failed chunk requests. Also verify that onClick$ uses the $ suffix — onClick (without $) binds a regular handler that won’t be serialized and won’t work after SSR.
useStore changes don’t trigger re-renders for nested objects — useStore is deeply reactive by default, but replacing a nested object breaks the proxy chain. Use state.nested.property = newValue (mutate) instead of state.nested = { ...state.nested, property: newValue } (replace). For arrays, push, splice, and index assignment work. Direct reassignment of the array (state.items = newArray) also works.
routeLoader$ data is stale after client-side navigation — routeLoader$ re-runs on each navigation by default. If the data appears stale, the issue is usually caching at the API level. Check if your API endpoint returns cached responses. For real-time data, use server$ with manual refresh instead of routeLoader$.
Build fails with “cannot capture” or “cannot serialize” — a non-serializable value is crossing a $ boundary. The error message usually names the offending value. Wrap it in noSerialize(), move it inside a useVisibleTask$, or restructure so it doesn’t cross the boundary.
For related framework issues, see Fix: SolidJS Not Working and Fix: SvelteKit Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: CodeMirror Not Working — Editor Not Rendering, Extensions Not Loading, or React State Out of Sync
How to fix CodeMirror 6 issues — basic setup, language and theme extensions, React integration, vim mode, collaborative editing, custom keybindings, and read-only mode.
Fix: GSAP Not Working — Animations Not Playing, ScrollTrigger Not Firing, or React Cleanup Issues
How to fix GSAP animation issues — timeline and tween basics, ScrollTrigger setup, React useGSAP hook, cleanup and context, SplitText, stagger animations, and Next.js integration.
Fix: i18next Not Working — Translations Missing, Language Not Switching, or Namespace Errors
How to fix i18next issues — react-i18next setup, translation file loading, namespace configuration, language detection, interpolation, pluralization, and Next.js integration.
Fix: ky Not Working — Requests Failing, Hooks Not Firing, or Retry Not Working
How to fix ky HTTP client issues — instance creation, hooks (beforeRequest, afterResponse), retry configuration, timeout handling, JSON parsing, error handling, and migration from fetch or axios.