Qwik City is Qwik's full-stack framework, providing complete routing, data fetching, and server-side functionality. Here are the core concepts and usage of Qwik City:
1. Introduction to Qwik City
Qwik City builds on top of Qwik and provides:
- File-system based routing
- Server-side data loading
- Form handling and validation
- Middleware support
- SEO optimization
- Internationalization support
2. Routing System
File System Routing
Qwik City uses file-system based routing, where file structure directly maps to URLs:
shellsrc/ ├── routes/ │ ├── index.tsx -> / │ ├── about/ │ │ └── index.tsx -> /about │ ├── products/ │ │ ├── index.tsx -> /products │ │ └── [id]/ │ │ └── index.tsx -> /products/:id │ └── layout.tsx -> Global layout
Dynamic Routing
tsx// routes/products/[id]/index.tsx import { component$ } from '@builder.io/qwik'; import { routeLoader$ } from '@builder.io/qwik-city'; import { useLocation } from '@builder.io/qwik-city'; export const useProductData = routeLoader$(async ({ params, url, env }) => { const response = await fetch(`https://api.example.com/products/${params.id}`); return response.json(); }); export default component$(() => { const product = useProductData(); const location = useLocation(); return ( <div> <h1>{product.value.name}</h1> <p>{product.value.description}</p> <p>Price: ${product.value.price}</p> </div> ); });
Nested Routing
tsx// routes/layout.tsx import { component$, Slot } from '@builder.io/qwik'; export default component$(() => { return ( <div> <header>Header</header> <main> <Slot /> </main> <footer>Footer</footer> </div> ); });
3. Data Loading
routeLoader$ - Server-Side Data Loading
tsximport { routeLoader$ } from '@builder.io/qwik-city'; export const useUserData = routeLoader$(async ({ params, url, env, requestEvent }) => { // Access request parameters const userId = params.id; // Access URL query parameters const searchParams = url.searchParams; const page = searchParams.get('page'); // Access environment variables const apiKey = env.get('API_KEY'); // Access request event const cookie = requestEvent.cookie.get('session'); const response = await fetch(`https://api.example.com/users/${userId}`); return response.json(); });
clientLoader$ - Client-Side Data Loading
tsximport { clientLoader$ } from '@builder.io/qwik-city'; export const useClientData = clientLoader$(async ({ params, navigate }) => { // Client-side data fetching const response = await fetch(`/api/data/${params.id}`); return response.json(); });
useResource$ - Component-Level Data Loading
tsximport { component$, useResource$ } from '@builder.io/qwik'; export const UserList = component$(() => { const users = useResource$(({ track, cleanup }) => { // Track dependencies track(() => /* dependencies */); // Cleanup function cleanup(() => { // Cleanup logic }); return fetch('https://api.example.com/users'); }); return ( <div> {users.value ? ( <ul> {users.value.map(user => <li key={user.id}>{user.name}</li>)} </ul> ) : ( <p>Loading...</p> )} </div> ); });
4. Form Handling
action$ - Server-Side Form Handling
tsximport { action$, zod$, z } from '@builder.io/qwik-city'; import { component$, Form } from '@builder.io/qwik-city'; // Define form validation export const useContactForm = action$(async (data, { requestEvent }) => { // Server-side processing logic const { name, email, message } = data; // Send email await sendEmail({ name, email, message }); return { success: true }; }, zod$({ name: z.string().min(2), email: z.string().email(), message: z.string().min(10) })); export default component$(() => { const action = useContactForm(); return ( <Form action={action}> <input name="name" placeholder="Name" /> <input name="email" type="email" placeholder="Email" /> <textarea name="message" placeholder="Message"></textarea> <button type="submit">Submit</button> {action.value?.success && <p>Message sent!</p>} </Form> ); });
clientAction$ - Client-Side Form Handling
tsximport { clientAction$ } from '@builder.io/qwik-city'; export const useClientAction = clientAction$(async (data) => { // Client-side processing logic console.log('Client action:', data); return { success: true }; });
5. Middleware
Request Middleware
tsx// routes/middleware.ts import { middleware$ } from '@builder.io/qwik-city'; export const onRequest = middleware$(async ({ requestEvent, next }) => { // Pre-request processing const url = requestEvent.url; const cookie = requestEvent.cookie.get('session'); if (!cookie && url.pathname !== '/login') { throw requestEvent.redirect(302, '/login'); } return next(); }); export const onResponse = middleware$(async ({ requestEvent, next }) => { // Post-response processing const response = await next(); // Add response headers response.headers.set('X-Custom-Header', 'value'); return response; });
6. SEO Optimization
Metadata Configuration
tsximport { component$ } from '@builder.io/qwik'; import { routeLoader$, useDocumentHead, useLocation } from '@builder.io/qwik-city'; export const useProductData = routeLoader$(async ({ params }) => { const response = await fetch(`https://api.example.com/products/${params.id}`); return response.json(); }); export default component$(() => { const product = useProductData(); return <div>{product.value.name}</div>; }); export const head = useDocumentHead$(({ resolveValue }) => { const product = resolveValue(useProductData); return { title: product.name, meta: [ { name: 'description', content: product.description }, { property: 'og:title', content: product.name }, { property: 'og:description', content: product.description }, { property: 'og:image', content: product.image } ] }; });
7. Internationalization
i18n Configuration
tsx// src/entry.ssr.tsx import { renderToStream } from '@builder.io/qwik/server'; import { Root } from './root'; import { I18nProvider } from 'qwik-speak'; export default function (opts) { return renderToStream(<Root />, { ...opts, containerAttributes: { lang: opts.lang } }); }
Using Translations
tsximport { component$ } from '@builder.io/qwik'; import { useSpeak } from 'qwik-speak'; export const MyComponent = component$(() => { const { t } = useSpeak(); return ( <div> <h1>{t('welcome.title')}</h1> <p>{t('welcome.description')}</p> </div> ); });
8. Best Practices
1. Properly Use routeLoader$ and useResource$
- routeLoader$: For page-level data, executes on server
- useResource$: For component-level data, can be dynamically refetched
2. Error Handling
tsxexport const useData = routeLoader$(async ({ params }) => { try { const response = await fetch(`https://api.example.com/data/${params.id}`); if (!response.ok) { throw new Error('Failed to fetch data'); } return response.json(); } catch (error) { throw requestEvent.redirect(302, '/error'); } });
3. Caching Strategy
tsxexport const useCachedData = routeLoader$(async ({ requestEvent }) => { const cacheKey = 'data'; const cached = requestEvent.sharedMap.get(cacheKey); if (cached) { return cached; } const data = await fetchData(); requestEvent.sharedMap.set(cacheKey, data); return data; });
Summary: Qwik City provides a complete full-stack development experience. Through routing, data loading, form handling, and other features, developers can quickly build high-performance web applications.