NexusCS

Qwik

Web Frameworks
Qwik is a resumable web framework with ~1KB initial JavaScript load. No hydration needed - apps resume where server left off.
featured

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