Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds
Torna alle collezioni
Guide35 min read

Qwik

Qwik is a revolutionary framework with resumability - instant loading without hydration for the fastest web applications.

Qwik - Instant Loading Web Applications

Czym jest Qwik?

Qwik to rewolucyjny framework JavaScript stworzony przez Miško Hevery (twórcę Angular) i zespół Builder.io. Wprowadza całkowicie nowe podejście do renderowania aplikacji webowych poprzez koncepcję "resumability" - zamiast tradycyjnej hydration, aplikacja Qwik jest interaktywna natychmiast po załadowaniu HTML, bez konieczności ładowania i wykonywania całego JavaScript z góry.

Framework rozwiązuje fundamentalny problem współczesnych aplikacji SPA: nawet przy SSR, użytkownik musi czekać na pobranie i wykonanie całego JS bundle, zanim strona stanie się interaktywna. Qwik eliminuje ten problem poprzez serializację stanu aplikacji bezpośrednio w HTML i lazy-loading JavaScript na poziomie pojedynczych event handlerów.

Problem: Hydration Tax

Jak działa tradycyjne SSR

W tradycyjnych frameworkach (React, Vue, Svelte) Server-Side Rendering przebiega następująco:

Code
TEXT
1. Serwer → Renderuje HTML
2. Przeglądarka → Pobiera HTML, wyświetla statyczną stronę
3. Przeglądarka → Pobiera CAŁY JavaScript bundle (50-500KB+)
4. Przeglądarka → Wykonuje JavaScript
5. Framework → "Hydratuje" stronę (re-renderuje wszystko w pamięci)
6. Strona → Staje się interaktywna

Problem: Kroki 3-5 to "hydration tax" - czas, który użytkownik musi czekać na interaktywność. Na wolnych urządzeniach mobilnych może to trwać kilka sekund.

Jak działa Qwik Resumability

Code
TEXT
1. Serwer → Renderuje HTML + serializuje stan w atrybutach
2. Przeglądarka → Pobiera HTML, wyświetla stronę
3. Strona → Jest NATYCHMIAST interaktywna
4. Przeglądarka → Pobiera JS tylko dla klikniętych elementów (lazy)

Korzyść: Brak hydration = natychmiastowa interaktywność.

Qwik vs React/Vue - porównanie wydajności

MetrykaQwikReactVueAngular
Initial JS~1KB50-100KB40-80KB80-150KB
Time to InteractiveInstant1-5s1-4s2-6s
HydrationBrak (resume)TakTakTak
Lazy loadingPer-listenerPer-routePer-routePer-route
Bundle growthLiniowyWykładniczyWykładniczyWykładniczy
SSR overheadMinimalnyWysokiŚredniWysoki

Instalacja i konfiguracja

Tworzenie nowego projektu

Code
Bash
# Inicjalizacja projektu z Qwik CLI
npm create qwik@latest

# Lub z pnpm
pnpm create qwik@latest

# Interaktywny wizard zapyta o:
# - Nazwę projektu
# - Starter template (podstawowy, z integrami)
# - Czy dodać Qwik City (routing/meta-framework)

Struktura projektu Qwik City

Code
TEXT
my-qwik-app/
├── src/
│   ├── components/           # Komponenty Qwik
│   │   ├── header/
│   │   │   └── header.tsx
│   │   └── footer/
│   │       └── footer.tsx
│   ├── routes/               # File-based routing
│   │   ├── index.tsx         # / (home page)
│   │   ├── about/
│   │   │   └── index.tsx     # /about
│   │   ├── blog/
│   │   │   ├── index.tsx     # /blog
│   │   │   └── [slug]/
│   │   │       └── index.tsx # /blog/:slug
│   │   └── layout.tsx        # Shared layout
│   ├── entry.ssr.tsx         # SSR entry point
│   └── root.tsx              # Root component
├── public/                   # Static assets
├── vite.config.ts            # Vite configuration
├── qwik.config.ts            # Qwik configuration
└── package.json

Podstawowa konfiguracja

TSvite.config.ts
TypeScript
// vite.config.ts
import { defineConfig } from 'vite'
import { qwikVite } from '@builder.io/qwik/optimizer'
import { qwikCity } from '@builder.io/qwik-city/vite'

export default defineConfig(() => {
  return {
    plugins: [qwikCity(), qwikVite()],
    server: {
      port: 5173,
    },
    preview: {
      port: 4173,
    },
  }
})

Podstawy Qwik - składnia

Komponenty z component$

TScomponents/counter.tsx
TypeScript
// components/counter.tsx
import { component$, useSignal } from '@builder.io/qwik'

// $ suffix oznacza, że funkcja jest lazy-loaded
export const Counter = component$(() => {
  // useSignal - reaktywny stan (fine-grained reactivity)
  const count = useSignal(0)

  return (
    <div class="counter">
      <p>Count: {count.value}</p>

      {/* onClick$ - handler lazy-loaded przy pierwszym kliknięciu */}
      <button onClick$={() => count.value++}>
        Increment
      </button>

      <button onClick$={() => count.value--}>
        Decrement
      </button>
    </div>
  )
})

Znaczenie symbolu $ (Dollar Sign)

Symbol $ w Qwik oznacza granicę lazy-loadingu. Wszystko po $ jest serializowane i ładowane tylko gdy potrzebne:

Code
TypeScript
import { component$, $, useSignal } from '@builder.io/qwik'

export const Example = component$(() => {
  const message = useSignal('')

  // $ tworzy lazy-loaded funkcję
  const handleClick = $(() => {
    // Ten kod jest w osobnym chunk i ładowany przy kliknięciu
    message.value = 'Button clicked!'
    console.log('This code was lazy-loaded')
  })

  // onClick$ automatycznie opakowuje w $
  return (
    <div>
      <button onClick$={handleClick}>Click me</button>
      <p>{message.value}</p>
    </div>
  )
})

useSignal - reaktywny stan

Code
TypeScript
import { component$, useSignal } from '@builder.io/qwik'

export const SignalExample = component$(() => {
  // Prymitywne wartości
  const count = useSignal(0)
  const name = useSignal('John')
  const isActive = useSignal(true)

  // Zmiana wartości
  const increment = $(() => {
    count.value++
  })

  const updateName = $((newName: string) => {
    name.value = newName
  })

  return (
    <div>
      <p>Count: {count.value}</p>
      <p>Name: {name.value}</p>
      <p>Active: {isActive.value ? 'Yes' : 'No'}</p>

      <button onClick$={increment}>+1</button>

      <input
        value={name.value}
        onInput$={(e) => name.value = (e.target as HTMLInputElement).value}
      />

      <button onClick$={() => isActive.value = !isActive.value}>
        Toggle
      </button>
    </div>
  )
})

useStore - reaktywne obiekty

Code
TypeScript
import { component$, useStore } from '@builder.io/qwik'

interface TodoItem {
  id: number
  text: string
  completed: boolean
}

interface State {
  todos: TodoItem[]
  filter: 'all' | 'active' | 'completed'
  newTodo: string
}

export const TodoApp = component$(() => {
  // useStore dla złożonych obiektów - głęboka reaktywność
  const state = useStore<State>({
    todos: [],
    filter: 'all',
    newTodo: '',
  })

  const addTodo = $(() => {
    if (state.newTodo.trim()) {
      // Bezpośrednia mutacja - reaktywna!
      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
    }
  })

  const filteredTodos = state.todos.filter(todo => {
    if (state.filter === 'active') return !todo.completed
    if (state.filter === 'completed') return todo.completed
    return true
  })

  return (
    <div class="todo-app">
      <input
        value={state.newTodo}
        onInput$={(e) => state.newTodo = (e.target as HTMLInputElement).value}
        onKeyDown$={(e) => e.key === 'Enter' && addTodo()}
        placeholder="Add todo..."
      />
      <button onClick$={addTodo}>Add</button>

      <div class="filters">
        {(['all', 'active', 'completed'] as const).map(filter => (
          <button
            key={filter}
            class={{ active: state.filter === filter }}
            onClick$={() => state.filter = filter}
          >
            {filter}
          </button>
        ))}
      </div>

      <ul>
        {filteredTodos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange$={() => toggleTodo(todo.id)}
            />
            <span class={{ completed: todo.completed }}>{todo.text}</span>
          </li>
        ))}
      </ul>
    </div>
  )
})

useComputed$ - computed values

Code
TypeScript
import { component$, useSignal, useComputed$ } from '@builder.io/qwik'

export const ComputedExample = component$(() => {
  const firstName = useSignal('John')
  const lastName = useSignal('Doe')
  const items = useSignal([10, 20, 30, 40, 50])

  // Computed - automatycznie przeliczane przy zmianie dependencies
  const fullName = useComputed$(() => {
    return `${firstName.value} ${lastName.value}`
  })

  const total = useComputed$(() => {
    return items.value.reduce((sum, item) => sum + item, 0)
  })

  const average = useComputed$(() => {
    const sum = items.value.reduce((s, i) => s + i, 0)
    return items.value.length > 0 ? sum / items.value.length : 0
  })

  return (
    <div>
      <p>Full Name: {fullName.value}</p>
      <p>Total: {total.value}</p>
      <p>Average: {average.value.toFixed(2)}</p>
    </div>
  )
})

useTask$ - side effects

Code
TypeScript
import { component$, useSignal, useTask$ } from '@builder.io/qwik'

export const TaskExample = component$(() => {
  const searchQuery = useSignal('')
  const results = useSignal<string[]>([])
  const isLoading = useSignal(false)

  // useTask$ - wykonuje się przy zmianie tracked signals
  useTask$(async ({ track, cleanup }) => {
    // track() rejestruje dependency
    const query = track(() => searchQuery.value)

    if (!query || query.length < 3) {
      results.value = []
      return
    }

    isLoading.value = true

    // Debounce
    const timeoutId = setTimeout(async () => {
      try {
        const response = await fetch(`/api/search?q=${query}`)
        results.value = await response.json()
      } finally {
        isLoading.value = false
      }
    }, 300)

    // cleanup - wywoływane przed następnym wykonaniem
    cleanup(() => clearTimeout(timeoutId))
  })

  return (
    <div>
      <input
        value={searchQuery.value}
        onInput$={(e) => searchQuery.value = (e.target as HTMLInputElement).value}
        placeholder="Search..."
      />

      {isLoading.value && <p>Searching...</p>}

      <ul>
        {results.value.map((result, i) => (
          <li key={i}>{result}</li>
        ))}
      </ul>
    </div>
  )
})

useVisibleTask$ - client-only effects

Code
TypeScript
import { component$, useSignal, useVisibleTask$ } from '@builder.io/qwik'

export const ClientOnlyExample = component$(() => {
  const windowWidth = useSignal(0)
  const mousePosition = useSignal({ x: 0, y: 0 })

  // useVisibleTask$ - wykonuje się TYLKO na kliencie
  // Używaj gdy potrzebujesz dostępu do DOM/Browser APIs
  useVisibleTask$(() => {
    // Ten kod nigdy nie wykona się na serwerze
    windowWidth.value = window.innerWidth

    const handleResize = () => {
      windowWidth.value = window.innerWidth
    }

    const handleMouseMove = (e: MouseEvent) => {
      mousePosition.value = { x: e.clientX, y: e.clientY }
    }

    window.addEventListener('resize', handleResize)
    window.addEventListener('mousemove', handleMouseMove)

    // Cleanup
    return () => {
      window.removeEventListener('resize', handleResize)
      window.removeEventListener('mousemove', handleMouseMove)
    }
  })

  return (
    <div>
      <p>Window width: {windowWidth.value}px</p>
      <p>Mouse: ({mousePosition.value.x}, {mousePosition.value.y})</p>
    </div>
  )
})

Qwik City - Meta-framework

File-based Routing

Code
TEXT
src/routes/
├── index.tsx              # → /
├── about/
│   └── index.tsx          # → /about
├── blog/
│   ├── index.tsx          # → /blog
│   └── [slug]/
│       └── index.tsx      # → /blog/:slug
├── api/
│   └── users/
│       └── index.ts       # → /api/users (server endpoint)
├── (auth)/                # Route group (nie dodaje do URL)
│   ├── login/
│   │   └── index.tsx      # → /login
│   └── register/
│       └── index.tsx      # → /register
└── layout.tsx             # Shared layout dla wszystkich routes

Strona z routeLoader$

TSsrc/routes/blog/[slug]/index.tsx
TypeScript
// src/routes/blog/[slug]/index.tsx
import { component$ } from '@builder.io/qwik'
import { routeLoader$, DocumentHead } from '@builder.io/qwik-city'

// routeLoader$ - data fetching na serwerze
export const usePost = routeLoader$(async ({ params, status }) => {
  const response = await fetch(`https://api.example.com/posts/${params.slug}`)

  if (!response.ok) {
    status(404)
    return null
  }

  return response.json() as Promise<{
    title: string
    content: string
    author: string
    date: string
  }>
})

export default component$(() => {
  // Dane są już załadowane na serwerze
  const post = usePost()

  if (!post.value) {
    return <div>Post not found</div>
  }

  return (
    <article class="blog-post">
      <h1>{post.value.title}</h1>
      <p class="meta">
        By {post.value.author} on {new Date(post.value.date).toLocaleDateString()}
      </p>
      <div class="content" dangerouslySetInnerHTML={post.value.content} />
    </article>
  )
})

// Dynamic head/meta
export const head: DocumentHead = ({ resolveValue }) => {
  const post = resolveValue(usePost)

  return {
    title: post?.title || 'Blog Post',
    meta: [
      { name: 'description', content: post?.content?.slice(0, 160) || '' },
      { property: 'og:title', content: post?.title || 'Blog' },
    ],
  }
}

routeAction$ - Server Actions

TSsrc/routes/contact/index.tsx
TypeScript
// src/routes/contact/index.tsx
import { component$ } from '@builder.io/qwik'
import { routeAction$, Form, zod$, z } from '@builder.io/qwik-city'

// Walidacja z Zod
const contactSchema = z.object({
  name: z.string().min(2, 'Name must be at least 2 characters'),
  email: z.string().email('Invalid email address'),
  message: z.string().min(10, 'Message must be at least 10 characters'),
})

// routeAction$ - server mutation
export const useContactForm = routeAction$(
  async (data, { fail }) => {
    try {
      // Wyślij do API
      const response = await fetch('https://api.example.com/contact', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data),
      })

      if (!response.ok) {
        return fail(500, { message: 'Failed to send message' })
      }

      return { success: true, message: 'Message sent successfully!' }
    } catch (error) {
      return fail(500, { message: 'Server error' })
    }
  },
  zod$(contactSchema)
)

export default component$(() => {
  const action = useContactForm()

  return (
    <div class="contact-form">
      <h1>Contact Us</h1>

      {/* Form - progressive enhancement, działa bez JS */}
      <Form action={action}>
        <div class="field">
          <label for="name">Name</label>
          <input type="text" id="name" name="name" required />
          {action.value?.fieldErrors?.name && (
            <span class="error">{action.value.fieldErrors.name}</span>
          )}
        </div>

        <div class="field">
          <label for="email">Email</label>
          <input type="email" id="email" name="email" required />
          {action.value?.fieldErrors?.email && (
            <span class="error">{action.value.fieldErrors.email}</span>
          )}
        </div>

        <div class="field">
          <label for="message">Message</label>
          <textarea id="message" name="message" rows={5} required />
          {action.value?.fieldErrors?.message && (
            <span class="error">{action.value.fieldErrors.message}</span>
          )}
        </div>

        <button type="submit" disabled={action.isRunning}>
          {action.isRunning ? 'Sending...' : 'Send Message'}
        </button>

        {action.value?.success && (
          <p class="success">{action.value.message}</p>
        )}

        {action.value?.failed && (
          <p class="error">{action.value.message}</p>
        )}
      </Form>
    </div>
  )
})

server$ - Server Functions

Code
TypeScript
import { component$, useSignal } from '@builder.io/qwik'
import { server$ } from '@builder.io/qwik-city'

// server$ - funkcja wykonywana na serwerze, wywoływana z klienta
const serverGreet = server$(async function (name: string) {
  // Ten kod ZAWSZE działa na serwerze
  // Masz dostęp do env, bazy danych, secrets
  const apiKey = this.env.get('API_KEY')

  console.log('Server log:', name, apiKey)

  // Symulacja operacji serwerowej
  await new Promise(resolve => setTimeout(resolve, 100))

  return {
    greeting: `Hello ${name} from server!`,
    timestamp: new Date().toISOString(),
  }
})

const fetchUserFromDB = server$(async function (userId: string) {
  // Bezpieczne operacje na bazie danych
  const db = await connectToDatabase()
  const user = await db.users.findById(userId)
  return user
})

export const ServerFunctionExample = component$(() => {
  const result = useSignal<{ greeting: string; timestamp: string } | null>(null)
  const isLoading = useSignal(false)

  const callServer = $(async () => {
    isLoading.value = true
    try {
      // Wywołanie server function z klienta
      result.value = await serverGreet('World')
    } finally {
      isLoading.value = false
    }
  })

  return (
    <div>
      <button onClick$={callServer} disabled={isLoading.value}>
        {isLoading.value ? 'Loading...' : 'Call Server'}
      </button>

      {result.value && (
        <div>
          <p>{result.value.greeting}</p>
          <small>At: {result.value.timestamp}</small>
        </div>
      )}
    </div>
  )
})

Layout i nested routing

TSsrc/routes/layout.tsx
TypeScript
// src/routes/layout.tsx
import { component$, Slot } from '@builder.io/qwik'
import { routeLoader$ } from '@builder.io/qwik-city'

// Global data loader
export const useCurrentUser = routeLoader$(async ({ cookie }) => {
  const token = cookie.get('auth-token')?.value
  if (!token) return null

  const response = await fetch('https://api.example.com/me', {
    headers: { Authorization: `Bearer ${token}` },
  })

  if (!response.ok) return null
  return response.json()
})

export default component$(() => {
  const user = useCurrentUser()

  return (
    <div class="app">
      <header>
        <nav>
          <a href="/">Home</a>
          <a href="/blog">Blog</a>
          <a href="/about">About</a>

          {user.value ? (
            <span>Welcome, {user.value.name}</span>
          ) : (
            <a href="/login">Login</a>
          )}
        </nav>
      </header>

      <main>
        {/* Slot renderuje child routes */}
        <Slot />
      </main>

      <footer>
        <p>© 2024 My App</p>
      </footer>
    </div>
  )
})
TSsrc/routes/dashboard/layout.tsx
TypeScript
// src/routes/dashboard/layout.tsx - Nested layout
import { component$, Slot } from '@builder.io/qwik'
import { routeLoader$, useLocation } from '@builder.io/qwik-city'

// Middleware - redirect if not authenticated
export const onRequest: RequestHandler = async ({ redirect, cookie }) => {
  const token = cookie.get('auth-token')?.value
  if (!token) {
    throw redirect(302, '/login')
  }
}

export default component$(() => {
  const location = useLocation()

  const navItems = [
    { href: '/dashboard', label: 'Overview' },
    { href: '/dashboard/projects', label: 'Projects' },
    { href: '/dashboard/settings', label: 'Settings' },
  ]

  return (
    <div class="dashboard-layout">
      <aside class="sidebar">
        <nav>
          {navItems.map(item => (
            <a
              key={item.href}
              href={item.href}
              class={{ active: location.url.pathname === item.href }}
            >
              {item.label}
            </a>
          ))}
        </nav>
      </aside>

      <div class="dashboard-content">
        <Slot />
      </div>
    </div>
  )
})

API Routes

TSsrc/routes/api/users/index.ts
TypeScript
// src/routes/api/users/index.ts
import type { RequestHandler } from '@builder.io/qwik-city'

export const onGet: RequestHandler = async ({ json, query }) => {
  const page = parseInt(query.get('page') || '1')
  const limit = parseInt(query.get('limit') || '10')

  const users = await db.users.findMany({
    skip: (page - 1) * limit,
    take: limit,
  })

  json(200, {
    users,
    pagination: { page, limit },
  })
}

export const onPost: RequestHandler = async ({ json, parseBody, status }) => {
  const body = await parseBody()

  if (!body?.email || !body?.name) {
    status(400)
    return json(400, { error: 'Missing required fields' })
  }

  const user = await db.users.create({
    data: { email: body.email, name: body.name },
  })

  json(201, user)
}
TSsrc/routes/api/users/[id]/index.ts
TypeScript
// src/routes/api/users/[id]/index.ts
import type { RequestHandler } from '@builder.io/qwik-city'

export const onGet: RequestHandler = async ({ params, json, status }) => {
  const user = await db.users.findById(params.id)

  if (!user) {
    status(404)
    return json(404, { error: 'User not found' })
  }

  json(200, user)
}

export const onPut: RequestHandler = async ({ params, parseBody, json, status }) => {
  const body = await parseBody()

  const user = await db.users.update({
    where: { id: params.id },
    data: body,
  })

  if (!user) {
    status(404)
    return json(404, { error: 'User not found' })
  }

  json(200, user)
}

export const onDelete: RequestHandler = async ({ params, json, status }) => {
  try {
    await db.users.delete({ where: { id: params.id } })
    json(200, { success: true })
  } catch {
    status(404)
    json(404, { error: 'User not found' })
  }
}

Integracje

Tailwind CSS

Code
Bash
npm run qwik add tailwind
Code
TypeScript
// Używanie Tailwind z Qwik
export const Button = component$<{
  variant?: 'primary' | 'secondary'
}>((props) => {
  const baseClasses = 'px-4 py-2 rounded-lg font-medium transition-colors'
  const variants = {
    primary: 'bg-blue-600 text-white hover:bg-blue-700',
    secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
  }

  return (
    <button class={`${baseClasses} ${variants[props.variant || 'primary']}`}>
      <Slot />
    </button>
  )
})

Prisma

Code
Bash
npm install prisma @prisma/client
npx prisma init
TSsrc/lib/prisma.ts
TypeScript
// src/lib/prisma.ts
import { PrismaClient } from '@prisma/client'

declare global {
  var prisma: PrismaClient | undefined
}

export const prisma = globalThis.prisma || new PrismaClient()

if (process.env.NODE_ENV !== 'production') {
  globalThis.prisma = prisma
}
TSsrc/routes/users/index.tsx
TypeScript
// src/routes/users/index.tsx
import { component$ } from '@builder.io/qwik'
import { routeLoader$ } from '@builder.io/qwik-city'
import { prisma } from '~/lib/prisma'

export const useUsers = routeLoader$(async () => {
  return prisma.user.findMany({
    select: {
      id: true,
      name: true,
      email: true,
      createdAt: true,
    },
    orderBy: { createdAt: 'desc' },
    take: 20,
  })
})

export default component$(() => {
  const users = useUsers()

  return (
    <div>
      <h1>Users</h1>
      <ul>
        {users.value.map(user => (
          <li key={user.id}>
            {user.name} - {user.email}
          </li>
        ))}
      </ul>
    </div>
  )
})

Auth (z Lucia)

TSsrc/lib/auth.ts
TypeScript
// src/lib/auth.ts
import { Lucia } from 'lucia'
import { PrismaAdapter } from '@lucia-auth/adapter-prisma'
import { prisma } from './prisma'

const adapter = new PrismaAdapter(prisma.session, prisma.user)

export const lucia = new Lucia(adapter, {
  sessionCookie: {
    attributes: {
      secure: process.env.NODE_ENV === 'production',
    },
  },
  getUserAttributes: (attributes) => ({
    email: attributes.email,
    name: attributes.name,
  }),
})
TSsrc/routes/login/index.tsx
TypeScript
// src/routes/login/index.tsx
import { component$ } from '@builder.io/qwik'
import { routeAction$, Form, zod$, z } from '@builder.io/qwik-city'
import { lucia } from '~/lib/auth'
import { prisma } from '~/lib/prisma'
import { verifyPassword } from '~/lib/password'

export const useLogin = routeAction$(
  async (data, { cookie, redirect, fail }) => {
    const user = await prisma.user.findUnique({
      where: { email: data.email },
    })

    if (!user || !await verifyPassword(data.password, user.passwordHash)) {
      return fail(401, { message: 'Invalid credentials' })
    }

    const session = await lucia.createSession(user.id, {})
    const sessionCookie = lucia.createSessionCookie(session.id)

    cookie.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes)

    throw redirect(302, '/dashboard')
  },
  zod$(z.object({
    email: z.string().email(),
    password: z.string().min(8),
  }))
)

export default component$(() => {
  const login = useLogin()

  return (
    <Form action={login}>
      <input type="email" name="email" placeholder="Email" required />
      <input type="password" name="password" placeholder="Password" required />
      <button type="submit">Login</button>
      {login.value?.failed && <p class="error">{login.value.message}</p>}
    </Form>
  )
})

Zaawansowane wzorce

Context

Code
TypeScript
import { component$, createContextId, useContextProvider, useContext, Slot } from '@builder.io/qwik'

// Definicja context
interface ThemeContext {
  theme: 'light' | 'dark'
  toggle: () => void
}

export const ThemeContextId = createContextId<ThemeContext>('theme')

// Provider
export const ThemeProvider = component$(() => {
  const themeStore = useStore<ThemeContext>({
    theme: 'light',
    toggle: $(() => {
      themeStore.theme = themeStore.theme === 'light' ? 'dark' : 'light'
    }),
  })

  useContextProvider(ThemeContextId, themeStore)

  return <Slot />
})

// Consumer
export const ThemeToggle = component$(() => {
  const theme = useContext(ThemeContextId)

  return (
    <button onClick$={theme.toggle}>
      Current: {theme.theme}
    </button>
  )
})

Resource i Suspense

Code
TypeScript
import { component$, useResource$, Resource } from '@builder.io/qwik'

export const AsyncDataExample = component$(() => {
  const userId = useSignal('1')

  // useResource$ - asynchroniczne dane z automatycznym SSR
  const userResource = useResource$(async ({ track, cleanup }) => {
    const id = track(() => userId.value)

    const controller = new AbortController()
    cleanup(() => controller.abort())

    const response = await fetch(`/api/users/${id}`, {
      signal: controller.signal,
    })

    return response.json()
  })

  return (
    <div>
      <select
        value={userId.value}
        onChange$={(e) => userId.value = (e.target as HTMLSelectElement).value}
      >
        <option value="1">User 1</option>
        <option value="2">User 2</option>
        <option value="3">User 3</option>
      </select>

      {/* Resource automatycznie obsługuje loading/error/success */}
      <Resource
        value={userResource}
        onPending={() => <p>Loading user...</p>}
        onRejected={(error) => <p>Error: {error.message}</p>}
        onResolved={(user) => (
          <div>
            <h2>{user.name}</h2>
            <p>{user.email}</p>
          </div>
        )}
      />
    </div>
  )
})

Deployment

Vercel

Code
Bash
npm run qwik add vercel-edge
# lub
npm run qwik add vercel-serverless

Cloudflare Pages

Code
Bash
npm run qwik add cloudflare-pages

Node.js Server

Code
Bash
npm run qwik add express
# lub
npm run qwik add fastify

Static Site Generation

Code
Bash
npm run qwik add static
npm run build.client
npm run build.server
npm run ssg

Performance porównanie

AplikacjaQwikNext.jsSvelteKit
E-commerce (50 produktów)3KB JS180KB JS45KB JS
Dashboard (10 widgets)2KB JS250KB JS60KB JS
Blog (10 postów)1.5KB JS120KB JS35KB JS
TTI (3G mobile)0.3s4.2s1.8s
LCP0.8s1.5s1.1s
CLS00.050.02

Cennik

  • 100% darmowy - MIT License
  • Builder.io Visual CMS: Opcjonalna integracja
    • Free tier: 1 projekt
    • Pro: $39/miesiąc

FAQ - Często zadawane pytania

Czy Qwik jest gotowy do produkcji?

Tak. Qwik jest w wersji stabilnej (v1.0+) i jest używany przez Builder.io oraz inne firmy w produkcji. Ma rosnącą społeczność i aktywny rozwój.

Jak Qwik się skaluje przy dużych aplikacjach?

Qwik skaluje się lepiej niż tradycyjne frameworki, ponieważ initial JS pozostaje stały (~1KB) niezależnie od rozmiaru aplikacji. JavaScript jest ładowany leniwie w miarę interakcji użytkownika.

Czy mogę używać bibliotek React z Qwik?

Qwik ma @builder.io/qwik-react który pozwala używać komponentów React w aplikacjach Qwik, ale tracisz wtedy korzyści resumability dla tych komponentów.

Jaka jest krzywa uczenia się Qwik?

Jeśli znasz React lub inne nowoczesne frameworki, Qwik jest łatwy do nauki. Składnia JSX jest znajoma. Główna różnica to zrozumienie koncepcji $ (lazy loading boundaries) i resumability.

Dlaczego symbol $ przy funkcjach?

$ oznacza granicę lazy-loadingu. Kompilator Qwik automatycznie wyodrębnia funkcje z $ do osobnych chunks, które są ładowane tylko gdy potrzebne. To klucz do osiągnięcia resumability.


Qwik - instant loading web applications

What is Qwik?

Qwik is a revolutionary JavaScript framework created by Misko Hevery (the creator of Angular) and the Builder.io team. It introduces a completely new approach to web application rendering through the concept of "resumability" - instead of traditional hydration, a Qwik application is interactive immediately after loading the HTML, without needing to download and execute all the JavaScript upfront.

The framework solves a fundamental problem of modern SPA applications: even with SSR, the user has to wait for the entire JS bundle to be downloaded and executed before the page becomes interactive. Qwik eliminates this problem by serializing the application state directly in the HTML and lazy-loading JavaScript at the level of individual event handlers.

Problem: hydration tax

How traditional SSR works

In traditional frameworks (React, Vue, Svelte), Server-Side Rendering works as follows:

Code
TEXT
1. Server → Renders HTML
2. Browser → Downloads HTML, displays a static page
3. Browser → Downloads the ENTIRE JavaScript bundle (50-500KB+)
4. Browser → Executes JavaScript
5. Framework → "Hydrates" the page (re-renders everything in memory)
6. Page → Becomes interactive

Problem: Steps 3-5 are the "hydration tax" - the time the user has to wait for interactivity. On slow mobile devices, this can take several seconds.

How Qwik resumability works

Code
TEXT
1. Server → Renders HTML + serializes state in attributes
2. Browser → Downloads HTML, displays the page
3. Page → Is IMMEDIATELY interactive
4. Browser → Downloads JS only for clicked elements (lazy)

Benefit: No hydration = instant interactivity.

Qwik vs React/Vue - performance comparison

MetricQwikReactVueAngular
Initial JS~1KB50-100KB40-80KB80-150KB
Time to InteractiveInstant1-5s1-4s2-6s
HydrationNone (resume)YesYesYes
Lazy loadingPer-listenerPer-routePer-routePer-route
Bundle growthLinearExponentialExponentialExponential
SSR overheadMinimalHighMediumHigh

Installation and configuration

Creating a new project

Code
Bash
# Initialize a project with Qwik CLI
npm create qwik@latest

# Or with pnpm
pnpm create qwik@latest

# The interactive wizard will ask about:
# - Project name
# - Starter template (basic, with integrations)
# - Whether to add Qwik City (routing/meta-framework)

Qwik City project structure

Code
TEXT
my-qwik-app/
├── src/
│   ├── components/           # Qwik components
│   │   ├── header/
│   │   │   └── header.tsx
│   │   └── footer/
│   │       └── footer.tsx
│   ├── routes/               # File-based routing
│   │   ├── index.tsx         # / (home page)
│   │   ├── about/
│   │   │   └── index.tsx     # /about
│   │   ├── blog/
│   │   │   ├── index.tsx     # /blog
│   │   │   └── [slug]/
│   │   │       └── index.tsx # /blog/:slug
│   │   └── layout.tsx        # Shared layout
│   ├── entry.ssr.tsx         # SSR entry point
│   └── root.tsx              # Root component
├── public/                   # Static assets
├── vite.config.ts            # Vite configuration
├── qwik.config.ts            # Qwik configuration
└── package.json

Basic configuration

TSvite.config.ts
TypeScript
// vite.config.ts
import { defineConfig } from 'vite'
import { qwikVite } from '@builder.io/qwik/optimizer'
import { qwikCity } from '@builder.io/qwik-city/vite'

export default defineConfig(() => {
  return {
    plugins: [qwikCity(), qwikVite()],
    server: {
      port: 5173,
    },
    preview: {
      port: 4173,
    },
  }
})

Qwik basics - syntax

Components with component$

TScomponents/counter.tsx
TypeScript
// components/counter.tsx
import { component$, useSignal } from '@builder.io/qwik'

// The $ suffix means the function is lazy-loaded
export const Counter = component$(() => {
  // useSignal - reactive state (fine-grained reactivity)
  const count = useSignal(0)

  return (
    <div class="counter">
      <p>Count: {count.value}</p>

      {/* onClick$ - handler is lazy-loaded on first click */}
      <button onClick$={() => count.value++}>
        Increment
      </button>

      <button onClick$={() => count.value--}>
        Decrement
      </button>
    </div>
  )
})

The meaning of the $ symbol (dollar sign)

The $ symbol in Qwik marks a lazy-loading boundary. Everything after $ is serialized and loaded only when needed:

Code
TypeScript
import { component$, $, useSignal } from '@builder.io/qwik'

export const Example = component$(() => {
  const message = useSignal('')

  // $ creates a lazy-loaded function
  const handleClick = $(() => {
    // This code is in a separate chunk and loaded on click
    message.value = 'Button clicked!'
    console.log('This code was lazy-loaded')
  })

  // onClick$ automatically wraps in $
  return (
    <div>
      <button onClick$={handleClick}>Click me</button>
      <p>{message.value}</p>
    </div>
  )
})

useSignal - reactive state

Code
TypeScript
import { component$, useSignal } from '@builder.io/qwik'

export const SignalExample = component$(() => {
  // Primitive values
  const count = useSignal(0)
  const name = useSignal('John')
  const isActive = useSignal(true)

  // Changing values
  const increment = $(() => {
    count.value++
  })

  const updateName = $((newName: string) => {
    name.value = newName
  })

  return (
    <div>
      <p>Count: {count.value}</p>
      <p>Name: {name.value}</p>
      <p>Active: {isActive.value ? 'Yes' : 'No'}</p>

      <button onClick$={increment}>+1</button>

      <input
        value={name.value}
        onInput$={(e) => name.value = (e.target as HTMLInputElement).value}
      />

      <button onClick$={() => isActive.value = !isActive.value}>
        Toggle
      </button>
    </div>
  )
})

useStore - reactive objects

Code
TypeScript
import { component$, useStore } from '@builder.io/qwik'

interface TodoItem {
  id: number
  text: string
  completed: boolean
}

interface State {
  todos: TodoItem[]
  filter: 'all' | 'active' | 'completed'
  newTodo: string
}

export const TodoApp = component$(() => {
  // useStore for complex objects - deep reactivity
  const state = useStore<State>({
    todos: [],
    filter: 'all',
    newTodo: '',
  })

  const addTodo = $(() => {
    if (state.newTodo.trim()) {
      // Direct mutation - reactive!
      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
    }
  })

  const filteredTodos = state.todos.filter(todo => {
    if (state.filter === 'active') return !todo.completed
    if (state.filter === 'completed') return todo.completed
    return true
  })

  return (
    <div class="todo-app">
      <input
        value={state.newTodo}
        onInput$={(e) => state.newTodo = (e.target as HTMLInputElement).value}
        onKeyDown$={(e) => e.key === 'Enter' && addTodo()}
        placeholder="Add todo..."
      />
      <button onClick$={addTodo}>Add</button>

      <div class="filters">
        {(['all', 'active', 'completed'] as const).map(filter => (
          <button
            key={filter}
            class={{ active: state.filter === filter }}
            onClick$={() => state.filter = filter}
          >
            {filter}
          </button>
        ))}
      </div>

      <ul>
        {filteredTodos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange$={() => toggleTodo(todo.id)}
            />
            <span class={{ completed: todo.completed }}>{todo.text}</span>
          </li>
        ))}
      </ul>
    </div>
  )
})

useComputed$ - computed values

Code
TypeScript
import { component$, useSignal, useComputed$ } from '@builder.io/qwik'

export const ComputedExample = component$(() => {
  const firstName = useSignal('John')
  const lastName = useSignal('Doe')
  const items = useSignal([10, 20, 30, 40, 50])

  // Computed - automatically recalculated when dependencies change
  const fullName = useComputed$(() => {
    return `${firstName.value} ${lastName.value}`
  })

  const total = useComputed$(() => {
    return items.value.reduce((sum, item) => sum + item, 0)
  })

  const average = useComputed$(() => {
    const sum = items.value.reduce((s, i) => s + i, 0)
    return items.value.length > 0 ? sum / items.value.length : 0
  })

  return (
    <div>
      <p>Full Name: {fullName.value}</p>
      <p>Total: {total.value}</p>
      <p>Average: {average.value.toFixed(2)}</p>
    </div>
  )
})

useTask$ - side effects

Code
TypeScript
import { component$, useSignal, useTask$ } from '@builder.io/qwik'

export const TaskExample = component$(() => {
  const searchQuery = useSignal('')
  const results = useSignal<string[]>([])
  const isLoading = useSignal(false)

  // useTask$ - runs when tracked signals change
  useTask$(async ({ track, cleanup }) => {
    // track() registers a dependency
    const query = track(() => searchQuery.value)

    if (!query || query.length < 3) {
      results.value = []
      return
    }

    isLoading.value = true

    // Debounce
    const timeoutId = setTimeout(async () => {
      try {
        const response = await fetch(`/api/search?q=${query}`)
        results.value = await response.json()
      } finally {
        isLoading.value = false
      }
    }, 300)

    // cleanup - called before the next execution
    cleanup(() => clearTimeout(timeoutId))
  })

  return (
    <div>
      <input
        value={searchQuery.value}
        onInput$={(e) => searchQuery.value = (e.target as HTMLInputElement).value}
        placeholder="Search..."
      />

      {isLoading.value && <p>Searching...</p>}

      <ul>
        {results.value.map((result, i) => (
          <li key={i}>{result}</li>
        ))}
      </ul>
    </div>
  )
})

useVisibleTask$ - client-only effects

Code
TypeScript
import { component$, useSignal, useVisibleTask$ } from '@builder.io/qwik'

export const ClientOnlyExample = component$(() => {
  const windowWidth = useSignal(0)
  const mousePosition = useSignal({ x: 0, y: 0 })

  // useVisibleTask$ - runs ONLY on the client
  // Use when you need access to DOM/Browser APIs
  useVisibleTask$(() => {
    // This code will never run on the server
    windowWidth.value = window.innerWidth

    const handleResize = () => {
      windowWidth.value = window.innerWidth
    }

    const handleMouseMove = (e: MouseEvent) => {
      mousePosition.value = { x: e.clientX, y: e.clientY }
    }

    window.addEventListener('resize', handleResize)
    window.addEventListener('mousemove', handleMouseMove)

    // Cleanup
    return () => {
      window.removeEventListener('resize', handleResize)
      window.removeEventListener('mousemove', handleMouseMove)
    }
  })

  return (
    <div>
      <p>Window width: {windowWidth.value}px</p>
      <p>Mouse: ({mousePosition.value.x}, {mousePosition.value.y})</p>
    </div>
  )
})

Qwik City - meta-framework

File-based routing

Code
TEXT
src/routes/
├── index.tsx              # → /
├── about/
│   └── index.tsx          # → /about
├── blog/
│   ├── index.tsx          # → /blog
│   └── [slug]/
│       └── index.tsx      # → /blog/:slug
├── api/
│   └── users/
│       └── index.ts       # → /api/users (server endpoint)
├── (auth)/                # Route group (doesn't add to URL)
│   ├── login/
│   │   └── index.tsx      # → /login
│   └── register/
│       └── index.tsx      # → /register
└── layout.tsx             # Shared layout for all routes

Page with routeLoader$

TSsrc/routes/blog/[slug]/index.tsx
TypeScript
// src/routes/blog/[slug]/index.tsx
import { component$ } from '@builder.io/qwik'
import { routeLoader$, DocumentHead } from '@builder.io/qwik-city'

// routeLoader$ - server-side data fetching
export const usePost = routeLoader$(async ({ params, status }) => {
  const response = await fetch(`https://api.example.com/posts/${params.slug}`)

  if (!response.ok) {
    status(404)
    return null
  }

  return response.json() as Promise<{
    title: string
    content: string
    author: string
    date: string
  }>
})

export default component$(() => {
  // Data is already loaded on the server
  const post = usePost()

  if (!post.value) {
    return <div>Post not found</div>
  }

  return (
    <article class="blog-post">
      <h1>{post.value.title}</h1>
      <p class="meta">
        By {post.value.author} on {new Date(post.value.date).toLocaleDateString()}
      </p>
      <div class="content" dangerouslySetInnerHTML={post.value.content} />
    </article>
  )
})

// Dynamic head/meta
export const head: DocumentHead = ({ resolveValue }) => {
  const post = resolveValue(usePost)

  return {
    title: post?.title || 'Blog Post',
    meta: [
      { name: 'description', content: post?.content?.slice(0, 160) || '' },
      { property: 'og:title', content: post?.title || 'Blog' },
    ],
  }
}

routeAction$ - server actions

TSsrc/routes/contact/index.tsx
TypeScript
// src/routes/contact/index.tsx
import { component$ } from '@builder.io/qwik'
import { routeAction$, Form, zod$, z } from '@builder.io/qwik-city'

// Validation with Zod
const contactSchema = z.object({
  name: z.string().min(2, 'Name must be at least 2 characters'),
  email: z.string().email('Invalid email address'),
  message: z.string().min(10, 'Message must be at least 10 characters'),
})

// routeAction$ - server mutation
export const useContactForm = routeAction$(
  async (data, { fail }) => {
    try {
      // Send to API
      const response = await fetch('https://api.example.com/contact', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data),
      })

      if (!response.ok) {
        return fail(500, { message: 'Failed to send message' })
      }

      return { success: true, message: 'Message sent successfully!' }
    } catch (error) {
      return fail(500, { message: 'Server error' })
    }
  },
  zod$(contactSchema)
)

export default component$(() => {
  const action = useContactForm()

  return (
    <div class="contact-form">
      <h1>Contact Us</h1>

      {/* Form - progressive enhancement, works without JS */}
      <Form action={action}>
        <div class="field">
          <label for="name">Name</label>
          <input type="text" id="name" name="name" required />
          {action.value?.fieldErrors?.name && (
            <span class="error">{action.value.fieldErrors.name}</span>
          )}
        </div>

        <div class="field">
          <label for="email">Email</label>
          <input type="email" id="email" name="email" required />
          {action.value?.fieldErrors?.email && (
            <span class="error">{action.value.fieldErrors.email}</span>
          )}
        </div>

        <div class="field">
          <label for="message">Message</label>
          <textarea id="message" name="message" rows={5} required />
          {action.value?.fieldErrors?.message && (
            <span class="error">{action.value.fieldErrors.message}</span>
          )}
        </div>

        <button type="submit" disabled={action.isRunning}>
          {action.isRunning ? 'Sending...' : 'Send Message'}
        </button>

        {action.value?.success && (
          <p class="success">{action.value.message}</p>
        )}

        {action.value?.failed && (
          <p class="error">{action.value.message}</p>
        )}
      </Form>
    </div>
  )
})

server$ - server functions

Code
TypeScript
import { component$, useSignal } from '@builder.io/qwik'
import { server$ } from '@builder.io/qwik-city'

// server$ - function executed on the server, called from the client
const serverGreet = server$(async function (name: string) {
  // This code ALWAYS runs on the server
  // You have access to env, database, secrets
  const apiKey = this.env.get('API_KEY')

  console.log('Server log:', name, apiKey)

  // Simulating a server operation
  await new Promise(resolve => setTimeout(resolve, 100))

  return {
    greeting: `Hello ${name} from server!`,
    timestamp: new Date().toISOString(),
  }
})

const fetchUserFromDB = server$(async function (userId: string) {
  // Safe database operations
  const db = await connectToDatabase()
  const user = await db.users.findById(userId)
  return user
})

export const ServerFunctionExample = component$(() => {
  const result = useSignal<{ greeting: string; timestamp: string } | null>(null)
  const isLoading = useSignal(false)

  const callServer = $(async () => {
    isLoading.value = true
    try {
      // Calling a server function from the client
      result.value = await serverGreet('World')
    } finally {
      isLoading.value = false
    }
  })

  return (
    <div>
      <button onClick$={callServer} disabled={isLoading.value}>
        {isLoading.value ? 'Loading...' : 'Call Server'}
      </button>

      {result.value && (
        <div>
          <p>{result.value.greeting}</p>
          <small>At: {result.value.timestamp}</small>
        </div>
      )}
    </div>
  )
})

Layout and nested routing

TSsrc/routes/layout.tsx
TypeScript
// src/routes/layout.tsx
import { component$, Slot } from '@builder.io/qwik'
import { routeLoader$ } from '@builder.io/qwik-city'

// Global data loader
export const useCurrentUser = routeLoader$(async ({ cookie }) => {
  const token = cookie.get('auth-token')?.value
  if (!token) return null

  const response = await fetch('https://api.example.com/me', {
    headers: { Authorization: `Bearer ${token}` },
  })

  if (!response.ok) return null
  return response.json()
})

export default component$(() => {
  const user = useCurrentUser()

  return (
    <div class="app">
      <header>
        <nav>
          <a href="/">Home</a>
          <a href="/blog">Blog</a>
          <a href="/about">About</a>

          {user.value ? (
            <span>Welcome, {user.value.name}</span>
          ) : (
            <a href="/login">Login</a>
          )}
        </nav>
      </header>

      <main>
        {/* Slot renders child routes */}
        <Slot />
      </main>

      <footer>
        <p>&copy; 2024 My App</p>
      </footer>
    </div>
  )
})
TSsrc/routes/dashboard/layout.tsx
TypeScript
// src/routes/dashboard/layout.tsx - Nested layout
import { component$, Slot } from '@builder.io/qwik'
import { routeLoader$, useLocation } from '@builder.io/qwik-city'

// Middleware - redirect if not authenticated
export const onRequest: RequestHandler = async ({ redirect, cookie }) => {
  const token = cookie.get('auth-token')?.value
  if (!token) {
    throw redirect(302, '/login')
  }
}

export default component$(() => {
  const location = useLocation()

  const navItems = [
    { href: '/dashboard', label: 'Overview' },
    { href: '/dashboard/projects', label: 'Projects' },
    { href: '/dashboard/settings', label: 'Settings' },
  ]

  return (
    <div class="dashboard-layout">
      <aside class="sidebar">
        <nav>
          {navItems.map(item => (
            <a
              key={item.href}
              href={item.href}
              class={{ active: location.url.pathname === item.href }}
            >
              {item.label}
            </a>
          ))}
        </nav>
      </aside>

      <div class="dashboard-content">
        <Slot />
      </div>
    </div>
  )
})

API routes

TSsrc/routes/api/users/index.ts
TypeScript
// src/routes/api/users/index.ts
import type { RequestHandler } from '@builder.io/qwik-city'

export const onGet: RequestHandler = async ({ json, query }) => {
  const page = parseInt(query.get('page') || '1')
  const limit = parseInt(query.get('limit') || '10')

  const users = await db.users.findMany({
    skip: (page - 1) * limit,
    take: limit,
  })

  json(200, {
    users,
    pagination: { page, limit },
  })
}

export const onPost: RequestHandler = async ({ json, parseBody, status }) => {
  const body = await parseBody()

  if (!body?.email || !body?.name) {
    status(400)
    return json(400, { error: 'Missing required fields' })
  }

  const user = await db.users.create({
    data: { email: body.email, name: body.name },
  })

  json(201, user)
}
TSsrc/routes/api/users/[id]/index.ts
TypeScript
// src/routes/api/users/[id]/index.ts
import type { RequestHandler } from '@builder.io/qwik-city'

export const onGet: RequestHandler = async ({ params, json, status }) => {
  const user = await db.users.findById(params.id)

  if (!user) {
    status(404)
    return json(404, { error: 'User not found' })
  }

  json(200, user)
}

export const onPut: RequestHandler = async ({ params, parseBody, json, status }) => {
  const body = await parseBody()

  const user = await db.users.update({
    where: { id: params.id },
    data: body,
  })

  if (!user) {
    status(404)
    return json(404, { error: 'User not found' })
  }

  json(200, user)
}

export const onDelete: RequestHandler = async ({ params, json, status }) => {
  try {
    await db.users.delete({ where: { id: params.id } })
    json(200, { success: true })
  } catch {
    status(404)
    json(404, { error: 'User not found' })
  }
}

Integrations

Tailwind CSS

Code
Bash
npm run qwik add tailwind
Code
TypeScript
// Using Tailwind with Qwik
export const Button = component$<{
  variant?: 'primary' | 'secondary'
}>((props) => {
  const baseClasses = 'px-4 py-2 rounded-lg font-medium transition-colors'
  const variants = {
    primary: 'bg-blue-600 text-white hover:bg-blue-700',
    secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
  }

  return (
    <button class={`${baseClasses} ${variants[props.variant || 'primary']}`}>
      <Slot />
    </button>
  )
})

Prisma

Code
Bash
npm install prisma @prisma/client
npx prisma init
TSsrc/lib/prisma.ts
TypeScript
// src/lib/prisma.ts
import { PrismaClient } from '@prisma/client'

declare global {
  var prisma: PrismaClient | undefined
}

export const prisma = globalThis.prisma || new PrismaClient()

if (process.env.NODE_ENV !== 'production') {
  globalThis.prisma = prisma
}
TSsrc/routes/users/index.tsx
TypeScript
// src/routes/users/index.tsx
import { component$ } from '@builder.io/qwik'
import { routeLoader$ } from '@builder.io/qwik-city'
import { prisma } from '~/lib/prisma'

export const useUsers = routeLoader$(async () => {
  return prisma.user.findMany({
    select: {
      id: true,
      name: true,
      email: true,
      createdAt: true,
    },
    orderBy: { createdAt: 'desc' },
    take: 20,
  })
})

export default component$(() => {
  const users = useUsers()

  return (
    <div>
      <h1>Users</h1>
      <ul>
        {users.value.map(user => (
          <li key={user.id}>
            {user.name} - {user.email}
          </li>
        ))}
      </ul>
    </div>
  )
})

Auth (with Lucia)

TSsrc/lib/auth.ts
TypeScript
// src/lib/auth.ts
import { Lucia } from 'lucia'
import { PrismaAdapter } from '@lucia-auth/adapter-prisma'
import { prisma } from './prisma'

const adapter = new PrismaAdapter(prisma.session, prisma.user)

export const lucia = new Lucia(adapter, {
  sessionCookie: {
    attributes: {
      secure: process.env.NODE_ENV === 'production',
    },
  },
  getUserAttributes: (attributes) => ({
    email: attributes.email,
    name: attributes.name,
  }),
})
TSsrc/routes/login/index.tsx
TypeScript
// src/routes/login/index.tsx
import { component$ } from '@builder.io/qwik'
import { routeAction$, Form, zod$, z } from '@builder.io/qwik-city'
import { lucia } from '~/lib/auth'
import { prisma } from '~/lib/prisma'
import { verifyPassword } from '~/lib/password'

export const useLogin = routeAction$(
  async (data, { cookie, redirect, fail }) => {
    const user = await prisma.user.findUnique({
      where: { email: data.email },
    })

    if (!user || !await verifyPassword(data.password, user.passwordHash)) {
      return fail(401, { message: 'Invalid credentials' })
    }

    const session = await lucia.createSession(user.id, {})
    const sessionCookie = lucia.createSessionCookie(session.id)

    cookie.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes)

    throw redirect(302, '/dashboard')
  },
  zod$(z.object({
    email: z.string().email(),
    password: z.string().min(8),
  }))
)

export default component$(() => {
  const login = useLogin()

  return (
    <Form action={login}>
      <input type="email" name="email" placeholder="Email" required />
      <input type="password" name="password" placeholder="Password" required />
      <button type="submit">Login</button>
      {login.value?.failed && <p class="error">{login.value.message}</p>}
    </Form>
  )
})

Advanced patterns

Context

Code
TypeScript
import { component$, createContextId, useContextProvider, useContext, Slot } from '@builder.io/qwik'

// Context definition
interface ThemeContext {
  theme: 'light' | 'dark'
  toggle: () => void
}

export const ThemeContextId = createContextId<ThemeContext>('theme')

// Provider
export const ThemeProvider = component$(() => {
  const themeStore = useStore<ThemeContext>({
    theme: 'light',
    toggle: $(() => {
      themeStore.theme = themeStore.theme === 'light' ? 'dark' : 'light'
    }),
  })

  useContextProvider(ThemeContextId, themeStore)

  return <Slot />
})

// Consumer
export const ThemeToggle = component$(() => {
  const theme = useContext(ThemeContextId)

  return (
    <button onClick$={theme.toggle}>
      Current: {theme.theme}
    </button>
  )
})

Resource and Suspense

Code
TypeScript
import { component$, useResource$, Resource } from '@builder.io/qwik'

export const AsyncDataExample = component$(() => {
  const userId = useSignal('1')

  // useResource$ - async data with automatic SSR
  const userResource = useResource$(async ({ track, cleanup }) => {
    const id = track(() => userId.value)

    const controller = new AbortController()
    cleanup(() => controller.abort())

    const response = await fetch(`/api/users/${id}`, {
      signal: controller.signal,
    })

    return response.json()
  })

  return (
    <div>
      <select
        value={userId.value}
        onChange$={(e) => userId.value = (e.target as HTMLSelectElement).value}
      >
        <option value="1">User 1</option>
        <option value="2">User 2</option>
        <option value="3">User 3</option>
      </select>

      {/* Resource automatically handles loading/error/success */}
      <Resource
        value={userResource}
        onPending={() => <p>Loading user...</p>}
        onRejected={(error) => <p>Error: {error.message}</p>}
        onResolved={(user) => (
          <div>
            <h2>{user.name}</h2>
            <p>{user.email}</p>
          </div>
        )}
      />
    </div>
  )
})

Deployment

Vercel

Code
Bash
npm run qwik add vercel-edge
# or
npm run qwik add vercel-serverless

Cloudflare Pages

Code
Bash
npm run qwik add cloudflare-pages

Node.js server

Code
Bash
npm run qwik add express
# or
npm run qwik add fastify

Static Site Generation

Code
Bash
npm run qwik add static
npm run build.client
npm run build.server
npm run ssg

Performance comparison

ApplicationQwikNext.jsSvelteKit
E-commerce (50 products)3KB JS180KB JS45KB JS
Dashboard (10 widgets)2KB JS250KB JS60KB JS
Blog (10 posts)1.5KB JS120KB JS35KB JS
TTI (3G mobile)0.3s4.2s1.8s
LCP0.8s1.5s1.1s
CLS00.050.02

Pricing

  • 100% free - MIT License
  • Builder.io Visual CMS: Optional integration
    • Free tier: 1 project
    • Pro: $39/month

FAQ - frequently asked questions

Is Qwik production-ready?

Yes. Qwik is in a stable version (v1.0+) and is used by Builder.io and other companies in production. It has a growing community and active development.

How does Qwik scale with large applications?

Qwik scales better than traditional frameworks because the initial JS remains constant (~1KB) regardless of application size. JavaScript is lazily loaded as the user interacts with the page.

Can I use React libraries with Qwik?

Qwik has @builder.io/qwik-react which allows you to use React components in Qwik applications, but you lose the resumability benefits for those components.

What is the learning curve for Qwik?

If you know React or other modern frameworks, Qwik is easy to learn. The JSX syntax is familiar. The main difference is understanding the $ concept (lazy loading boundaries) and resumability.

Why the $ symbol on functions?

$ marks a lazy-loading boundary. The Qwik compiler automatically extracts functions with $ into separate chunks that are loaded only when needed. This is the key to achieving resumability.