install
source · Clone the upstream repo
git clone https://github.com/ComeOnOliver/skillshub
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/TerminalSkills/skills/qwik" ~/.claude/skills/comeonoliver-skillshub-qwik && rm -rf "$T"
manifest:
skills/TerminalSkills/skills/qwik/SKILL.mdsource content
Qwik
Qwik eliminates hydration by serializing the application state into HTML. JavaScript loads lazily on user interaction, not on page load. This means near-zero JS on initial load regardless of app complexity.
Installation
# Create Qwik project with Qwik City (meta-framework) npm create qwik@latest cd my-app npm install npm run dev
Project Structure
# Qwik City project layout src/ ├── entry.ssr.tsx # SSR entry ├── root.tsx # Root component ├── global.css ├── routes/ # File-based routing │ ├── layout.tsx # Root layout │ ├── index.tsx # / page │ └── articles/ │ ├── index.tsx # /articles │ └── [slug]/ │ └── index.tsx # /articles/:slug ├── components/ # Reusable components │ └── article-card/ │ └── article-card.tsx └── lib/ # Utilities
Components
// src/components/article-card/article-card.tsx — Qwik component import { component$ } from '@builder.io/qwik'; import { Link } from '@builder.io/qwik-city'; interface Props { title: string; slug: string; excerpt: string; } export const ArticleCard = component$<Props>((props) => { return ( <article> <Link href={`/articles/${props.slug}`}> <h2>{props.title}</h2> </Link> <p>{props.excerpt}</p> </article> ); });
Signals and State
// src/routes/counter/index.tsx — signals and reactivity import { component$, useSignal, useComputed$, useTask$ } from '@builder.io/qwik'; export default component$(() => { const count = useSignal(0); const doubled = useComputed$(() => count.value * 2); useTask$(({ track }) => { track(() => count.value); console.log(`Count changed to ${count.value}`); }); return ( <div> <p>Count: {count.value} (doubled: {doubled.value})</p> <button onClick$={() => count.value++}>+1</button> </div> ); });
Data Loading with routeLoader$
// src/routes/articles/index.tsx — server-side data loading import { component$ } from '@builder.io/qwik'; import { routeLoader$ } from '@builder.io/qwik-city'; import { ArticleCard } from '~/components/article-card/article-card'; export const useArticles = routeLoader$(async ({ env }) => { const res = await fetch(`${env.get('API_URL')}/articles`); return res.json() as Promise<Article[]>; }); export default component$(() => { const articles = useArticles(); return ( <div> <h1>Articles</h1> {articles.value.map((article) => ( <ArticleCard key={article.id} title={article.title} slug={article.slug} excerpt={article.excerpt} /> ))} </div> ); });
Server Actions
// src/routes/articles/new/index.tsx — form with server action import { component$ } from '@builder.io/qwik'; import { routeAction$, Form, zod$, z } from '@builder.io/qwik-city'; export const useCreateArticle = routeAction$( async (data, { redirect, env }) => { const res = await fetch(`${env.get('API_URL')}/articles`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); if (!res.ok) return { success: false, error: 'Failed to create' }; throw redirect(302, '/articles'); }, zod$({ title: z.string().min(1).max(200), body: z.string().min(1), }) ); export default component$(() => { const action = useCreateArticle(); return ( <Form action={action}> <input name="title" placeholder="Title" required /> <textarea name="body" placeholder="Body" required /> <button type="submit">Create</button> {action.value?.error && <p>{action.value.error}</p>} </Form> ); });
Layouts
// src/routes/layout.tsx — root layout import { component$, Slot } from '@builder.io/qwik'; import { Link } from '@builder.io/qwik-city'; export default component$(() => { return ( <div> <header> <nav> <Link href="/">Home</Link> <Link href="/articles">Articles</Link> </nav> </header> <main> <Slot /> </main> </div> ); });
Middleware
// src/routes/admin/layout.tsx — auth middleware via onRequest import { type RequestHandler } from '@builder.io/qwik-city'; export const onRequest: RequestHandler = async ({ cookie, redirect }) => { const token = cookie.get('session')?.value; if (!token) throw redirect(302, '/login'); };
useStore for Complex State
// src/routes/dashboard/index.tsx — store for nested reactive state import { component$, useStore, $ } from '@builder.io/qwik'; export default component$(() => { const state = useStore({ articles: [] as Article[], filter: '', loading: false, }); const fetchArticles = $(async () => { state.loading = true; const res = await fetch('/api/articles'); state.articles = await res.json(); state.loading = false; }); return ( <div> <button onClick$={fetchArticles}>Load</button> <input bind:value={state.filter} placeholder="Filter..." /> {state.loading ? <p>Loading...</p> : ( state.articles .filter((a) => a.title.includes(state.filter)) .map((a) => <div key={a.id}>{a.title}</div>) )} </div> ); });
Deployment
# Add deployment adapter npm run qwik add cloudflare-pages # or: vercel, netlify, node-server, deno npm run build npm run deploy
Key Patterns
- Use
suffix on functions ($
,onClick$
,component$
) — it marks serialization boundariesrouteLoader$ - Use
for primitives,useSignal
for objects — similar to Solid's signal/store splituseStore
runs on the server during SSR — data is serialized into HTMLrouteLoader$
handles form submissions server-side with Zod validationrouteAction$- JavaScript loads lazily per interaction — no hydration step
- Use
handlers in layouts for server-side middleware (auth, redirects)onRequest - Deploy anywhere: Cloudflare, Vercel, Netlify, Deno, Node — via adapters