Jotai完全ガイド 2025 - アトミック状態管理の新時代
Jotai v2を徹底解説。アトミックアプローチによる状態管理の革新、primitive・derived atoms、React 18対応、TypeScript完全サポート、実践的なパターンまで、モダンなReactアプリケーション開発の全てを網羅します。
Qwik Cityを徹底解説。ファイルベースルーティング、SSR/SSG、ローダー・アクション、ミドルウェア、エンドポイントから実践的な実装パターンまで、モダンWebアプリケーション開発の最前線を完全網羅します。
Qwik City は、高速な初期読み込みとゼロハイドレーションを特徴とするメタフレームワークです。Builder.io によって開発され、Vercelや Cloudflareなどの企業でも活用されています。従来の Reactや Vue ベースのメタフレームワークとは異なるアプローチで、パフォーマンスを重視するプロジェクトで注目されています。
Next.js、Nuxtはハイドレーションで初期読み込みが遅い
Builder.ioがリザマビリティを実現
メタフレームワーク機能を追加
大手企業での本格導入開始
実用性と組み合わせやすさが向上
チャートを読み込み中...
特徴 | Qwik City | Next.js | Nuxt 3 | SvelteKit |
---|---|---|---|---|
初期読み込み | ~3KB | ~13KB | ~8KB | ~6KB |
ハイドレーション | なし | 必要 | 必要 | 必要 |
Time to Interactive | 高速 | ~2秒 | ~1.5秒 | ~1秒 |
ファイルベースルーティング | ✓ | ✓ | ✓ | ✓ |
SSR/SSG | ✓ | ✓ | ✓ | ✓ |
Edge Functions | ✓ | ✓ | ✓ | 実験的 |
TypeScript | 完全対応 | 完全対応 | 完全対応 | 完全対応 |
学習コスト | 中程度 | 高い | 中程度 | 低い |
エコシステム | 発展中 | 成熟 | 成熟 | 成熟 |
// src/routes/index.tsx
import { component$ } from '@builder.io/qwik';
export default component$(() => {
return (
<div>
<h1>Welcome to Qwik City!</h1>
<p>このページは src/routes/index.tsx で定義されています</p>
</div>
);
});
Pages は各ルートの主要コンテンツを定義します。ファイルベースルーティングにより、ディレクトリ構造がそのまま URL 構造になります。
// src/routes/layout.tsx
import { component$, Slot } from '@builder.io/qwik';
export default component$(() => {
return (
<div>
<header>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
</header>
<main>
<Slot /> {/* ページコンテンツが挿入される */}
</main>
<footer>
<p>© 2025 My Qwik City Site</p>
</footer>
</div>
);
});
Layouts は複数ページで共有される構造を定義します。ネストしたレイアウトも可能で、階層的な設計ができます。
// src/routes/posts/index.tsx
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
export const usePosts = routeLoader$(async () => {
// サーバーサイドでデータを取得
const response = await fetch('https://api.example.com/posts');
const posts = await response.json();
return posts;
});
export default component$(() => {
const posts = usePosts();
return (
<div>
<h1>Blog Posts</h1>
{posts.value.map((post: any) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
);
});
Loaders はサーバーサイドでデータを事前取得し、ページに注入します。SEO に優れ、初期表示が高速です。
src/routes/
├── index.tsx # / (ホームページ)
├── about/
│ └── index.tsx # /about
├── blog/
│ ├── index.tsx # /blog
│ ├── [slug]/
│ │ └── index.tsx # /blog/[slug] (動的ルート)
│ └── [...slug]/
│ └── index.tsx # /blog/[...slug] (catch-all)
├── api/
│ ├── users/
│ │ ├── index.ts # GET /api/users
│ │ └── [id]/
│ │ └── index.ts # GET /api/users/[id]
│ └── auth.ts # /api/auth
├── layout.tsx # ルートレイアウト
└── error.tsx # エラーページ
// src/routes/blog/[slug]/index.tsx
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
import type { RequestHandler } from '@builder.io/qwik-city';
export const usePost = routeLoader$(async ({ params }) => {
const slug = params.slug;
try {
const response = await fetch(`https://api.blog.com/posts/${slug}`);
if (!response.ok) {
throw new Error('Post not found');
}
return await response.json();
} catch (error) {
throw new Error(`Failed to load post: ${slug}`);
}
});
export default component$(() => {
const post = usePost();
return (
<article>
<h1>{post.value.title}</h1>
<div class="meta">
<time>{post.value.publishedAt}</time>
<span>by {post.value.author}</span>
</div>
<div dangerouslySetInnerHTML={post.value.content} />
</article>
);
});
// メタデータの設定
export const head: RequestHandler = ({ params, data }) => {
const post = data?.post;
return {
title: post?.title,
meta: [
{ name: 'description', content: post?.excerpt },
{ property: 'og:title', content: post?.title },
{ property: 'og:description', content: post?.excerpt },
],
};
};
// 従来のクライアントサイド処理
import { useState } from 'react';
const ContactForm = () => {
const [formData, setFormData] = useState({});
const [loading, setLoading] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
if (!response.ok) throw new Error('Failed');
// 成功処理
alert('送信成功');
} catch (error) {
alert('送信失敗');
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
{/* フォーム要素 */}
</form>
);
};
// Qwik City Actions
import { component$ } from '@builder.io/qwik';
import { routeAction$, Form } from '@builder.io/qwik-city';
import { z } from 'zod';
// バリデーションスキーマ
const ContactSchema = z.object({
name: z.string().min(2, '名前は2文字以上で入力してください'),
email: z.string().email('有効なメールアドレスを入力してください'),
message: z.string().min(10, 'メッセージは10文字以上で入力してください')
});
export const useContactAction = routeAction$(async (data, { redirect }) => {
// サーバーサイドバリデーション
const result = ContactSchema.safeParse(data);
if (!result.success) {
return {
success: false,
errors: result.error.flatten().fieldErrors
};
}
// メール送信処理
try {
await sendEmail({
to: 'contact@example.com',
subject: 'New Contact Form Submission',
body: `Name: ${result.data.name}\nEmail: ${result.data.email}\nMessage: ${result.data.message}`
});
throw redirect(302, '/contact/success');
} catch (error) {
return {
success: false,
error: 'メール送信に失敗しました'
};
}
});
export default component$(() => {
const contactAction = useContactAction();
return (
<Form action={contactAction}>
<div>
<label for="name">名前</label>
<input id="name" name="name" required />
{contactAction.value?.errors?.name && (
<span class="error">{contactAction.value.errors.name[0]}</span>
)}
</div>
<div>
<label for="email">メールアドレス</label>
<input id="email" name="email" type="email" required />
{contactAction.value?.errors?.email && (
<span class="error">{contactAction.value.errors.email[0]}</span>
)}
</div>
<div>
<label for="message">メッセージ</label>
<textarea id="message" name="message" required />
{contactAction.value?.errors?.message && (
<span class="error">{contactAction.value.errors.message[0]}</span>
)}
</div>
<button type="submit">
{contactAction.isRunning ? '送信中...' : '送信'}
</button>
{contactAction.value?.error && (
<div class="error">{contactAction.value.error}</div>
)}
</Form>
);
});
// 従来のクライアントサイド処理
import { useState } from 'react';
const ContactForm = () => {
const [formData, setFormData] = useState({});
const [loading, setLoading] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
if (!response.ok) throw new Error('Failed');
// 成功処理
alert('送信成功');
} catch (error) {
alert('送信失敗');
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
{/* フォーム要素 */}
</form>
);
};
// Qwik City Actions
import { component$ } from '@builder.io/qwik';
import { routeAction$, Form } from '@builder.io/qwik-city';
import { z } from 'zod';
// バリデーションスキーマ
const ContactSchema = z.object({
name: z.string().min(2, '名前は2文字以上で入力してください'),
email: z.string().email('有効なメールアドレスを入力してください'),
message: z.string().min(10, 'メッセージは10文字以上で入力してください')
});
export const useContactAction = routeAction$(async (data, { redirect }) => {
// サーバーサイドバリデーション
const result = ContactSchema.safeParse(data);
if (!result.success) {
return {
success: false,
errors: result.error.flatten().fieldErrors
};
}
// メール送信処理
try {
await sendEmail({
to: 'contact@example.com',
subject: 'New Contact Form Submission',
body: `Name: ${result.data.name}\nEmail: ${result.data.email}\nMessage: ${result.data.message}`
});
throw redirect(302, '/contact/success');
} catch (error) {
return {
success: false,
error: 'メール送信に失敗しました'
};
}
});
export default component$(() => {
const contactAction = useContactAction();
return (
<Form action={contactAction}>
<div>
<label for="name">名前</label>
<input id="name" name="name" required />
{contactAction.value?.errors?.name && (
<span class="error">{contactAction.value.errors.name[0]}</span>
)}
</div>
<div>
<label for="email">メールアドレス</label>
<input id="email" name="email" type="email" required />
{contactAction.value?.errors?.email && (
<span class="error">{contactAction.value.errors.email[0]}</span>
)}
</div>
<div>
<label for="message">メッセージ</label>
<textarea id="message" name="message" required />
{contactAction.value?.errors?.message && (
<span class="error">{contactAction.value.errors.message[0]}</span>
)}
</div>
<button type="submit">
{contactAction.isRunning ? '送信中...' : '送信'}
</button>
{contactAction.value?.error && (
<div class="error">{contactAction.value.error}</div>
)}
</Form>
);
});
// src/middleware.ts
import type { RequestHandler } from '@builder.io/qwik-city';
import { jwt } from './utils/jwt';
export const onRequest: RequestHandler = async ({ request, next, redirect, sharedMap }) => {
// CORS設定
const response = await next();
response.headers.set('Access-Control-Allow-Origin', '*');
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
return response;
};
// 認証が必要なルート用ミドルウェア
export const onGet: RequestHandler = async ({ request, next, redirect, cookie }) => {
const url = new URL(request.url);
// 保護されたルートのチェック
if (url.pathname.startsWith('/dashboard')) {
const token = cookie.get('auth-token')?.value;
if (!token) {
throw redirect(302, '/login');
}
try {
const payload = await jwt.verify(token);
// ユーザー情報をコンテキストに追加
request.locals = { user: payload };
} catch (error) {
cookie.delete('auth-token');
throw redirect(302, '/login');
}
}
return next();
};
// レート制限ミドルウェア
const rateLimitMap = new Map<string, { count: number; resetTime: number }>();
export const onPost: RequestHandler = async ({ request, next, clientConn }) => {
const clientIP = clientConn.ip;
const now = Date.now();
const windowMs = 15 * 60 * 1000; // 15分
const maxRequests = 100;
const current = rateLimitMap.get(clientIP);
if (!current || now > current.resetTime) {
rateLimitMap.set(clientIP, {
count: 1,
resetTime: now + windowMs
});
} else {
current.count++;
if (current.count > maxRequests) {
return new Response('Too Many Requests', {
status: 429,
headers: {
'Retry-After': Math.ceil((current.resetTime - now) / 1000).toString()
}
});
}
}
return next();
};
// src/routes/api/users/index.ts
import type { RequestHandler } from '@builder.io/qwik-city';
import { z } from 'zod';
const UserSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
age: z.number().min(0).max(120)
});
// GET /api/users
export const onGet: RequestHandler = async ({ query, json }) => {
const page = parseInt(query.get('page') || '1');
const limit = parseInt(query.get('limit') || '10');
const search = query.get('search') || '';
try {
const users = await db.users.findMany({
where: search ? {
OR: [
{ name: { contains: search, mode: 'insensitive' } },
{ email: { contains: search, mode: 'insensitive' } }
]
} : {},
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' }
});
const total = await db.users.count();
json(200, {
users,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit)
}
});
} catch (error) {
json(500, { error: 'Internal server error' });
}
};
// POST /api/users
export const onPost: RequestHandler = async ({ request, json }) => {
try {
const body = await request.json();
const result = UserSchema.safeParse(body);
if (!result.success) {
json(400, {
error: 'Validation failed',
details: result.error.flatten().fieldErrors
});
return;
}
const user = await db.users.create({
data: result.data
});
json(201, { user });
} catch (error) {
if (error.code === 'P2002') { // Unique constraint violation
json(409, { error: 'Email already exists' });
} else {
json(500, { error: 'Internal server error' });
}
}
};
// PUT /api/users/[id]
export const onPut: RequestHandler = async ({ params, request, json }) => {
const userId = parseInt(params.id);
if (isNaN(userId)) {
json(400, { error: 'Invalid user ID' });
return;
}
try {
const body = await request.json();
const result = UserSchema.partial().safeParse(body);
if (!result.success) {
json(400, {
error: 'Validation failed',
details: result.error.flatten().fieldErrors
});
return;
}
const user = await db.users.update({
where: { id: userId },
data: result.data
});
json(200, { user });
} catch (error) {
if (error.code === 'P2025') { // Record not found
json(404, { error: 'User not found' });
} else {
json(500, { error: 'Internal server error' });
}
}
};
// DELETE /api/users/[id]
export const onDelete: RequestHandler = async ({ params, json }) => {
const userId = parseInt(params.id);
if (isNaN(userId)) {
json(400, { error: 'Invalid user ID' });
return;
}
try {
await db.users.delete({
where: { id: userId }
});
json(204, null);
} catch (error) {
if (error.code === 'P2025') {
json(404, { error: 'User not found' });
} else {
json(500, { error: 'Internal server error' });
}
}
};
// 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({
// SSG設定
trailingSlash: false,
// プリレンダリング設定
staticGenerate: {
include: [
'/blog/*', // ブログ記事をプリレンダリング
'/docs/*', // ドキュメントをプリレンダリング
],
// 動的ルートの生成
async dynamicRoutes() {
const posts = await fetchAllPosts();
return posts.map(post => `/blog/${post.slug}`);
},
// サイトマップ生成
sitemap: {
hostname: 'https://example.com',
changefreq: 'weekly',
priority: 0.8
}
}
}),
qwikVite({
// 最適化オプション
entryStrategy: {
type: 'smart' // スマート分割
}
})
],
// ビルド最適化
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['@builder.io/qwik']
}
}
}
},
// プレビュー設定
preview: {
headers: {
'Cache-Control': 'public, max-age=31536000, immutable'
}
}
};
});
// src/routes/blog/[slug]/index.tsx
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
export const usePost = routeLoader$(async ({ params, cacheControl }) => {
// キャッシュ制御の設定
cacheControl({
// ブラウザキャッシュ: 1時間
maxAge: 3600,
// CDNキャッシュ: 24時間
sMaxAge: 86400,
// 再検証時でも古いキャッシュを返す
staleWhileRevalidate: 86400 * 7,
// プライベートキャッシュは無効
private: false
});
const post = await fetchPost(params.slug);
return post;
});
// エッジキャッシュの活用
export const onGet: RequestHandler = async ({ cacheControl, request }) => {
const url = new URL(request.url);
// 静的アセットの長期キャッシュ
if (url.pathname.startsWith('/assets/')) {
cacheControl({
maxAge: 31536000, // 1年
immutable: true
});
}
// APIエンドポイントの短期キャッシュ
if (url.pathname.startsWith('/api/')) {
cacheControl({
maxAge: 300, // 5分
sMaxAge: 600, // CDNで10分
staleWhileRevalidate: 3600
});
}
};
src/
├── components/ # 共通コンポーネント
│ ├── ui/ # UIコンポーネント
│ │ ├── button/
│ │ ├── form/
│ │ └── modal/
│ └── layout/ # レイアウトコンポーネント
├── routes/ # Qwik City ルート
│ ├── (auth)/ # 認証グループ
│ │ ├── login/
│ │ └── register/
│ ├── (dashboard)/ # ダッシュボードグループ
│ │ ├── layout.tsx
│ │ ├── overview/
│ │ └── settings/
│ ├── api/ # APIエンドポイント
│ │ ├── auth/
│ │ ├── users/
│ │ └── posts/
│ ├── blog/ # ブログセクション
│ └── docs/ # ドキュメントセクション
├── lib/ # ユーティリティライブラリ
│ ├── auth.ts # 認証関連
│ ├── db.ts # データベース接続
│ ├── email.ts # メール送信
│ └── validation.ts # バリデーション
├── types/ # TypeScript型定義
│ ├── auth.ts
│ ├── user.ts
│ └── api.ts
├── hooks/ # カスタムフック
│ ├── useAuth.ts
│ └── useApi.ts
├── styles/ # スタイル定義
│ ├── global.css
│ └── components.css
├── middleware.ts # グローバルミドルウェア
└── entry.ssr.tsx # SSRエントリポイント
// src/types/api.ts
export interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
pagination?: {
page: number;
limit: number;
total: number;
pages: number;
};
}
export interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user';
createdAt: string;
updatedAt: string;
}
export interface Post {
id: number;
title: string;
slug: string;
content: string;
excerpt: string;
publishedAt: string;
author: User;
tags: string[];
}
// src/lib/api.ts
import type { ApiResponse, User, Post } from '~/types/api';
export class ApiClient {
private baseUrl: string;
constructor(baseUrl: string = '/api') {
this.baseUrl = baseUrl;
}
async get<T>(endpoint: string): Promise<ApiResponse<T>> {
const response = await fetch(`${this.baseUrl}${endpoint}`);
return response.json();
}
async post<T>(endpoint: string, data: unknown): Promise<ApiResponse<T>> {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
return response.json();
}
// ユーザー関連API
async getUsers(params?: { page?: number; limit?: number; search?: string }) {
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', params.page.toString());
if (params?.limit) searchParams.set('limit', params.limit.toString());
if (params?.search) searchParams.set('search', params.search);
return this.get<User[]>(`/users?${searchParams}`);
}
// 投稿関連API
async getPosts(params?: { page?: number; limit?: number }) {
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', params.page.toString());
if (params?.limit) searchParams.set('limit', params.limit.toString());
return this.get<Post[]>(`/posts?${searchParams}`);
}
}
// グローバルAPIクライアント
export const api = new ApiClient();
// adapters/vercel/vite.config.ts
import { vercelEdgeAdapter } from '@builder.io/qwik-city/adapters/vercel-edge';
export default defineConfig(() => {
return {
plugins: [
qwikCity({
adapters: [vercelEdgeAdapter()]
})
]
};
});
// vercel.json
{
"functions": {
"src/routes/**/*.ts": {
"runtime": "edge"
}
},
"rewrites": [
{ "source": "/(.*)", "destination": "/src/entry.vercel-edge.tsx" }
]
}
// adapters/cloudflare-pages/vite.config.ts
import { cloudflarePagesAdapter } from '@builder.io/qwik-city/adapters/cloudflare-pages';
export default defineConfig(() => {
return {
plugins: [
qwikCity({
adapters: [cloudflarePagesAdapter()]
})
]
};
});
# wrangler.toml
name = "qwik-city-app"
compatibility_date = "2023-10-30"
[build]
command = "npm run build"
publish = "dist"
[[pages_plugins]]
binding = "DB"
database_name = "my-database"
database_id = "xxx-xxx-xxx"
// adapters/netlify/vite.config.ts
import { netlifyAdapter } from '@builder.io/qwik-city/adapters/netlify-edge';
export default defineConfig(() => {
return {
plugins: [
qwikCity({
adapters: [netlifyAdapter()]
})
]
};
});
# netlify.toml
[build]
publish = "dist"
command = "npm run build"
[[edge_functions]]
path = "/*"
function = "entry.netlify-edge"
Qwik City は、ゼロハイドレーション、高速な初期読み込み、独特な開発体験を特徴とするメタフレームワークです。2025 年現在、パフォーマンスを重視するプロジェクトで微増しています。
以下のようなプロジェクトで Qwik City の活用を検討できます:
導入時の考慮点:
リザマビリティというアプローチにより、Qwik City は従来のメタフレームワークとは異なるパフォーマンス特性を提供します。ただし、新しい概念の理解とエコシステムの成熟を待つ必要があります。
Qwik City は、パフォーマンスを重視する Web アプリケーションの開発で有用な選択肢となりつつあります。エコシステムの成熟とともに、今後の採用が期待されています。