Frontend Builder について
Frontend Builder は、React と Next.js のフロントエンド開発者向けのスキルガイドです。コンポーネント設計、状態管理、データ取得、フォーム、性能改善の実践方法と、フレームワーク選定の指針を紹介します。
主な機能
- 01React と Next.js の選定指針
- 02再利用可能なコンポーネント設計
- 03状態管理の判断ガイド
- 04データ取得とフォーム処理
- 05レンダリングとアセットの最適化
活用シーン
- 01新規フロントエンドのフレームワーク選定
- 02保守しやすい UI コンポーネントの構成
- 03アプリの状態管理方式の選択
- 04フロントエンドの読み込みと描画の改善
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ daffy0208/ ai-dev-standards --skill frontend-builderインストール後、Claude Code で/frontend-builderを入力して読み込まれたか確認します。
同梱ファイルmanifest.yamlREADME.md
詳細
フロントエンドビルダー
保守性とパフォーマンスに優れた React および Next.js のフロントエンドを構築します。
基本原則
1. コンポーネントの構成
UI を小さく、再利用可能で、単一の目的を持つコンポーネントに分割します。
2. 状態の局所化
状態は、できるだけ使用する場所の近くに保持します。
3. デフォルトで高いパフォーマンス
レンダリング、コード分割、アセットの読み込みを最適化します。
4. 開発者体験
分かりやすい命名、一貫したパターン、役立つエラーを心がけます。
フレームワークの選択
React(Vite)と Next.js の比較
React + Vite を使う場合:
- クライアント側のみのアプリケーション
- SEO 要件がない
- シンプルなデプロイ(静的ホスティング)
- 初期セットアップをより迅速に行いたい
Next.js を使う場合:
- SEO が重要(マーケティングサイト、ブログ、e コマース)
- サーバーサイドレンダリングが必要
- API ルートが必要
- ファイルベースのルーティングを使いたい
- 画像の最適化が重要
ほとんどのプロジェクトにおすすめ:Next.js(App Router)
コンポーネントアーキテクチャ
コンポーネントの種類
1. ページコンポーネント(ルートのエントリーポイント):
// app/users/page.tsx (Next.js App Router)
export default function UsersPage() {
return (
<div>
<Header />
<UserList />
<Footer />
</div>
)
}
2. 機能コンポーネント(ビジネスロジック):
// components/features/UserList.tsx
export function UserList() {
const { data, isLoading } = useUsers()
if (isLoading) return <LoadingSpinner />
return (
<div>
{data.map(user => <UserCard key={user.id} user={user} />)}
</div>
)
}
3. UI コンポーネント(再利用可能で、ビジネスロジックを持たない):
// components/ui/button.tsx
export function Button({ children, variant = 'primary', ...props }) {
return (
<button
className={cn(buttonVariants[variant])}
{...props}
>
{children}
</button>
)
}
コンポーネントのベストプラクティス
// ✅ Good: Small, focused, typed
interface UserProfileProps {
user: User
onEdit?: () => void
}
export function UserProfile({ user, onEdit }: UserProfileProps) {
return (
<div className="flex gap-4">
<Avatar src={user.avatar} alt={user.name} />
<UserDetails user={user} />
{onEdit && <Button onClick={onEdit}>Edit</Button>}
</div>
)
}
// ❌ Bad: Giant, untyped, unclear
export function UserProfile(props) {
// 500 lines of JSX, multiple responsibilities
return <div>...</div>
}
状態管理
判断フロー
How many components need this state?
│
├─ One component → useState
├─ Parent + children → Props or useState + props
├─ Siblings → Lift to common parent
├─ Widely used (theme, auth) → Context API
└─ Complex app state → Zustand or Redux
ローカル状態(useState)
// For component-level state
function Counter() {
const [count, setCount] = useState(0)
const [isOpen, setIsOpen] = useState(false)
return (
<div>
<button onClick={() => setCount(count + 1)}>{count}</button>
</div>
)
}
Context API
// For app-wide state (theme, auth, user)
const UserContext = createContext<UserContextType | undefined>(undefined)
export function UserProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null)
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
)
}
export function useUser() {
const context = useContext(UserContext)
if (!context) throw new Error('useUser must be within UserProvider')
return context
}
Zustand(複雑な状態管理におすすめ)
import { create } from 'zustand'
interface CounterStore {
count: number
increment: () => void
decrement: () => void
reset: () => void
}
export const useCounterStore = create<CounterStore>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 })
}))
// Usage
function Counter() {
const { count, increment } = useCounterStore()
return <button onClick={increment}>{count}</button>
}
データ取得
React Query(おすすめ)
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
// Query (GET)
function Users() {
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
staleTime: 5 * 60 * 1000 // 5 minutes
})
if (isLoading) return <LoadingSpinner />
if (error) return <ErrorMessage error={error} />
return <UserList users={data} />
}
// Mutation (POST, PUT, DELETE)
function CreateUser() {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: createUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] })
}
})
return (
<button onClick={() => mutation.mutate({ name: 'John' })}>
Create User
</button>
)
}
Next.js サーバーコンポーネント(App Router)
// app/users/page.tsx
// Server Component - fetches on server
export default async function UsersPage() {
const users = await fetchUsers() // Runs on server
return <UserList users={users} />
}
// Client Component - for interactivity
'use client'
export function UserList({ users }: { users: User[] }) {
const [selected, setSelected] = useState<string | null>(null)
return (
<div>
{users.map(user => (
<UserCard
key={user.id}
user={user}
onClick={() => setSelected(user.id)}
/>
))}
</div>
)
}
フォーム処理
React Hook Form(おすすめ)
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
const loginSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(8, 'Password must be at least 8 characters')
})
type LoginForm = z.infer<typeof loginSchema>
function LoginForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting }
} = useForm<LoginForm>({
resolver: zodResolver(loginSchema)
})
const onSubmit = async (data: LoginForm) => {
await login(data)
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input
{...register('email')}
type="email"
placeholder="Email"
className="border p-2"
/>
{errors.email && (
<span className="text-red-500">{errors.email.message}</span>
)}
</div>
<div>
<input
{...register('password')}
type="password"
placeholder="Password"
className="border p-2"
/>
{errors.password && (
<span className="text-red-500">{errors.password.message}</span>
)}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Logging in...' : 'Login'}
</button>
</form>
)
}
スタイリング
Tailwind CSS(おすすめ)
// Install: @shadcn/ui for component library
function Button({ variant = 'primary', children, ...props }) {
return (
<button
className={cn(
'px-4 py-2 rounded font-medium transition-colors',
{
'bg-blue-500 text-white hover:bg-blue-600': variant === 'primary',
'bg-gray-200 text-gray-900 hover:bg-gray-300': variant === 'secondary',
'bg-red-500 text-white hover:bg-red-600': variant === 'danger'
}
)}
{...props}
>
{children}
</button>
)
}
CSS Modules(代替手段)
// Button.module.css
.button {
padding: 0.5rem 1rem;
border-radius: 0.25rem;
}
.primary {
background-color: blue;
color: white;
}
// Button.tsx
import styles from './Button.module.css'
export function Button({ variant = 'primary', children }) {
return (
<button className={`${styles.button} ${styles[variant]}`}>
{children}
</button>
)
}
パフォーマンスの最適化
React の最適化
import { memo, useMemo, useCallback } from 'react'
// 1. Memoize expensive calculations
function DataTable({ data }) {
const sortedData = useMemo(
() => data.sort((a, b) => a.name.localeCompare(b.name)),
[data]
)
return <Table data={sortedData} />
}
// 2. Memoize callbacks
function Parent() {
const handleClick = useCallback(() => {
console.log('Clicked')
}, [])
return <ExpensiveChild onClick={handleClick} />
}
// 3. Memoize components
const ExpensiveChild = memo(function ExpensiveChild({ onClick }) {
return <button onClick={onClick}>Click</button>
})
Next.js の最適化
// 1. Image optimization
import Image from 'next/image'
<Image
src="/photo.jpg"
alt="Photo"
width={500}
height={300}
priority // Above the fold
/>
// 2. Font optimization
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
export default function RootLayout({ children }) {
return (
<html className={inter.className}>
<body>{children}</body>
</html>
)
}
// 3. Dynamic imports (code splitting)
import dynamic from 'next/dynamic'
const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
loading: () => <LoadingSpinner />
})
エラー処理
エラーバウンダリー
'use client'
import { Component, ReactNode } from 'react'
interface Props {
children: ReactNode
fallback?: ReactNode
}
interface State {
hasError: boolean
error?: Error
}
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error }
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div className="p-4 bg-red-50 border border-red-200">
<h2 className="text-red-800">Something went wrong</h2>
<p className="text-red-600">{this.state.error?.message}</p>
</div>
)
}
return this.props.children
}
}
Next.js のエラー処理
// app/error.tsx
'use client'
export default function Error({
error,
reset
}: {
error: Error
reset: () => void
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
)
}
フォルダー構成
Next.js App Router
app/
├── (auth)/ # Route group (auth pages)
│ ├── login/
│ └── signup/
├── (dashboard)/ # Route group (dashboard)
│ ├── layout.tsx
│ ├── page.tsx
│ └── settings/
├── api/ # API routes
│ └── users/
│ └── route.ts
└── layout.tsx # Root layout
components/
├── ui/ # shadcn/ui components
│ ├── button.tsx
│ ├── input.tsx
│ └── dialog.tsx
├── features/ # Feature components
│ ├── UserList.tsx
│ └── UserProfile.tsx
└── layouts/ # Layout components
├── Header.tsx
└── Footer.tsx
lib/
├── utils.ts # Utility functions
├── api.ts # API client
└── validation.ts # Zod schemas
hooks/
├── useUser.ts
└── useDebounce.ts
stores/
└── userStore.ts # Zustand stores
TypeScript のベストプラクティス
// 1. Type component props
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger'
children: ReactNode
onClick?: () => void
}
export function Button({ variant = 'primary', children, onClick }: ButtonProps) {
return <button onClick={onClick}>{children}</button>
}
// 2. Type API responses
interface User {
id: string
name: string
email: string
}
async function fetchUsers(): Promise<User[]> {
const res = await fetch('/api/users')
return res.json()
}
// 3. Type state
const [user, setUser] = useState<User | null>(null)
const [isLoading, setIsLoading] = useState<boolean>(false)
まとめ
優れたフロントエンドでは、次の点を実践します。
- ✅ ほとんどのプロジェクトで Next.js を使用する(SEO、パフォーマンス、開発者体験)
- ✅ UI を小さく、型付けされたコンポーネントに分割する
- ✅ 適切な状態管理を選ぶ(useState → Context → Zustand)
- ✅ サーバー状態には React Query を使用する
- ✅ Tailwind CSS + shadcn/ui でスタイリングする
- ✅ メモ化とコード分割で最適化する
- ✅ エラーバウンダリーでエラーを適切に処理する
- ✅ 一貫したフォルダー構成に従う
関連リソース
関連スキル:
api-designer- 利用するバックエンド API の設計ux-designer- 実装する UX デザインの作成deployment-advisor- Next.js/React アプリのホスティング
関連パターン:
META/DECISION-FRAMEWORK.md- フロントエンドフレームワークの選択STANDARDS/architecture-patterns/component-patterns.md- コンポーネント設計パターン(作成時)
関連プレイブック:
PLAYBOOKS/setup-nextjs-project.md- Next.js プロジェクトのセットアップ(作成時)PLAYBOOKS/optimize-frontend-performance.md- パフォーマンスの最適化(作成時)
コメント
Markdown 対応