前提: Next.js (App Router) + TypeScript で開発するプロジェクトを想定しています。
各セクションにコードスニペットを付けて、実際にどのように実装へ落とし込むかイメージできるようにしました。全コードは 最小実装 を意識し、読みやすさを優先しています。
export / import
で組み合わせ。// components/Button.tsx
import { ReactNode } from 'react'
export type ButtonProps = {
children: ReactNode
onClick?: () => void
variant?: 'primary' | 'secondary'
}
export const Button = ({
children,
onClick,
variant = 'primary',
}: ButtonProps) => (
<button
onClick={onClick}
className={
variant === 'primary'
? 'rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700'
: 'rounded bg-gray-200 px-4 py-2 text-gray-900 hover:bg-gray-300'
}
>
{children}
</button>
)
// components/TodoItem.tsx
import { Button } from './Button'
type Todo = { id: string; title: string; done: boolean }
type TodoItemProps = {
todo: Todo
onToggle: (id: string) => void
}
export const TodoItem = ({ todo, onToggle }: TodoItemProps) => (
<li className="flex items-center gap-2">
<input
type="checkbox"
checked={todo.done}
onChange={() => onToggle(todo.id)}
/>
<span className={todo.done ? 'text-gray-400 line-through' : ''}>
{todo.title}
</span>
</li>
)
// hooks/useFilteredTodos.ts
import { useMemo } from 'react'
import { Todo } from '../types'
export const useFilteredTodos = (todos: Todo[], showDone: boolean) =>
useMemo(
() => todos.filter((t) => (showDone ? true : !t.done)),
[todos, showDone]
)
// components/TodoList.client.tsx (Client Component)
'use client'
import { useState } from 'react'
import { useFilteredTodos } from '../hooks/useFilteredTodos'
import { Todo } from '../types'
import { Button } from './Button'
import { TodoItem } from './TodoItem'
// components/TodoList.client.tsx (Client Component)
// components/TodoList.client.tsx (Client Component)
type Props = { initialTodos: Todo[] }
export default function TodoList({ initialTodos }: Props) {
const [todos, setTodos] = useState(initialTodos)
const [showDone, setShowDone] = useState(false)
const filtered = useFilteredTodos(todos, showDone)
const toggle = (id: string) =>
setTodos((prev) =>
prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t))
)
return (
<section>
<Button variant="secondary" onClick={() => setShowDone((s) => !s)}>
{showDone ? 'すべて表示' : '未完了のみ'}
</Button>
<ul className="mt-4 space-y-2">
{filtered.map((todo) => (
<TodoItem key={todo.id} todo={todo} onToggle={toggle} />
))}
</ul>
</section>
)
}
Context
や state 管理ライブラリ (Zustand / Jotai など) を検討。// components/ThemeProvider.client.tsx
'use client'
import { createContext, useContext, useState } from 'react'
// components/ThemeProvider.client.tsx
// components/ThemeProvider.client.tsx
type Theme = 'light' | 'dark'
const ThemeCtx = createContext<{
theme: Theme
toggle: () => void
} | null>(null)
export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
const [theme, setTheme] = useState<Theme>('light')
return (
<ThemeCtx.Provider
value={{
theme,
toggle: () => setTheme((t) => (t === 'light' ? 'dark' : 'light')),
}}
>
{children}
</ThemeCtx.Provider>
)
}
export const useTheme = () => {
const ctx = useContext(ThemeCtx)
if (!ctx) throw new Error('useTheme must be used inside <ThemeProvider />')
return ctx
}
// components/Card.tsx
import { ReactNode } from 'react'
export const Card = ({ children }: { children: ReactNode }) => (
<div className="rounded-2xl border p-6 shadow">{children}</div>
)
// 使用例
<Card>
<h2 className="mb-2 text-xl font-semibold">ユーザープロフィール</h2>
<ProfileDetails user={user} />
</Card>
"use client"
を指定した Client Component に分割。// app/todos/page.tsx (Server Component)
import TodoList from '../../components/TodoList.client'
import { db } from '../../lib/db'
export default async function TodosPage() {
const initialTodos = await db.todo.findMany()
return (
<main className="mx-auto max-w-xl p-8">
<h1 className="mb-4 text-2xl font-bold">TODO</h1>
{/* Client Component にデータを渡す */}
<TodoList initialTodos={initialTodos} />
</main>
)
}
function Greeting({ name }: { name: string }) {
const [count, setCount] = useState(0) // トップレベル
return (
<p onClick={() => setCount((c) => c + 1)}>
こんにちは {name}! ({count})
</p>
)
}
function Foo({ flag }: { flag: boolean }) {
if (flag) {
// 🚫 ここで useEffect を呼ぶと hook の呼び出し順が変わる
useEffect(() => {
console.log('flag が true')
}, [])
}
return <div>NG Example</div>
}
これら 6 つの指針は互いに連携しています。たとえば 単一責任 を保つことで Hooks の再利用 が進み、Client / Server コンポーネントの分離 が自然と進行します。常に「最小責任・最小バンドル」を意識して構築すると、メンテナンス性とパフォーマンスの両立が可能です。