【React】プロジェクト構築からCRUD実装・Next.js移行まで完全ガイド
Reactは、Metaが開発したコンポーネントベースのUIライブラリです。Next.jsのようなフルスタックフレームワークとは異なり、UI構築に特化しているため、シンプルで柔軟な開発が可能です。
この記事では、ReactとNext.jsの違いからプロジェクト構築、DB連携によるCRUD実装、サーバー状態管理、そしてNext.jsからの移行手順までを体系的に解説します。
ReactとNext.jsの違い
ReactとNext.jsは「ライブラリ vs フレームワーク」という根本的な違いがあります。
| 項目 | React | Next.js |
|---|---|---|
| 種類 | UIライブラリ | フレームワーク |
| ルーティング | 別途導入(React Router等) | 組み込み(ファイルベース) |
| レンダリング | CSRのみ | CSR / SSR / SSG / ISR |
| SEO | 工夫が必要 | SSR/SSGで対応しやすい |
| API | 別途バックエンド必要 | API Routes内蔵 |
| 設定 | 自分で構築 | ゼロコンフィグ |
React単体を選ぶメリット
CSRのみに集中でき、「サーバー?クライアント?」を意識する必要がありません。学習コストも低く抑えられます。
ルーティング、状態管理、ビルドツール、デプロイ先をすべて自由に選択できます。フレームワークの規約に縛られません。
React単体が向いているケース
- 管理画面・ダッシュボード: SEO不要、認証後のみアクセス
- 社内ツール: 検索エンジンに載せる必要なし
- SPA: 画面遷移が多いインタラクティブなアプリ
- 既存バックエンドがある: Rails、Laravel、Go等と組み合わせ
- Electronアプリ: デスクトップアプリ化
デプロイの柔軟性
React(静的ビルド)はどこでもホスト可能です。
- S3 + CloudFront
- GitHub Pages
- Firebase Hosting
- Nginx / Apache
Next.jsのSSR機能を使うとNode.js環境が必要になりますが、React単体の静的ビルドならその制約がありません。
Next.jsは「Reactの上に構築されたフレームワーク」です。Next.jsを使う=Reactも使っていることになります。「SSRが不要でバックエンドが別にある」ならReact単体で十分です。
Reactプロジェクトの始め方
Vite + Reactでプロジェクト作成
npm create vite@latest my-app -- --template react-tscreate-react-app は非推奨(メンテナンス終了)のため、Viteを使用します。
cd my-app
npm install npm run dev よく使うパッケージ
ルーティング
npm install react-router-dom
import { BrowserRouter, Routes, Route } from 'react-router-dom';
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</BrowserRouter>
状態管理
| パッケージ | 特徴 | 用途 |
|---|---|---|
| Zustand | シンプル、軽量 | 小〜中規模 |
| Jotai | アトムベース | 細かい状態管理 |
| TanStack Query | サーバー状態管理 | API連携 |
# Zustand(おすすめ)
npm install zustand
# TanStack Query(API連携に必須級)
npm install @tanstack/react-query
フォーム
npm install react-hook-form zod @hookform/resolvers
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
UIライブラリ
| パッケージ | 特徴 |
|---|---|
| shadcn/ui | コピペ式、カスタマイズ自由 |
| MUI | 豊富なコンポーネント |
| Chakra UI | シンプルで使いやすい |
| Mantine | 高機能、モダン |
スタイリング
# Tailwind CSS
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
# CSS Modules(Vite標準対応、追加インストール不要)
典型的な構成例
npm create vite@latest my-app -- --template react-ts
cd my-app
# 基本セット
npm install react-router-dom zustand @tanstack/react-query axios
# フォーム
npm install react-hook-form zod @hookform/resolvers
# スタイリング
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
推奨フォルダ構成
src/
├── components/ # 共通コンポーネント
│ └── ui/ # ボタン、入力等
├── features/ # 機能単位
│ └── auth/
│ ├── components/
│ ├── hooks/
│ └── api.ts
├── hooks/ # 共通フック
├── lib/ # ユーティリティ
├── pages/ # ページコンポーネント
├── stores/ # Zustand等
└── types/ # 型定義
- ビルド Vite
- ルーティング react-router-dom
- サーバー状態 @tanstack/react-query
- クライアント状態 zustand
- フォーム react-hook-form + zod
- スタイル Tailwind CSS
DB接続によるCRUD実装
ReactはDBに直接接続できないため、フロントエンド(React)とバックエンド(Express)を分離した構成で実装します。
フロントエンドからDBに直接接続すると、接続情報(パスワード等)がユーザーに見えるため、必ずバックエンドを経由してください。
全体構成
my-app/
├── backend/ # Express + SQLite
│ ├── index.js
│ ├── app.db # 自動生成
│ └── package.json
└── frontend/ # React + Vite
├── src/
│ ├── api.ts
│ └── App.tsx
└── package.json
バックエンド構築
セットアップ
mkdir my-app && cd my-app
mkdir backend && cd backend
npm init -y
npm install express cors better-sqlite3
backend/index.js
const express = require('express');
const cors = require('cors');
const Database = require('better-sqlite3');
const app = express();
const db = new Database('app.db');
// ミドルウェア
app.use(cors());
app.use(express.json());
// テーブル作成
db.exec(`
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
completed INTEGER DEFAULT 0
)
`);
// 全件取得(Read)
app.get('/api/todos', (req, res) => {
const todos = db.prepare('SELECT * FROM todos').all();
res.json(todos);
});
// 作成(Create)
app.post('/api/todos', (req, res) => {
const { title } = req.body;
const result = db.prepare('INSERT INTO todos (title) VALUES (?)').run(title);
res.json({ id: result.lastInsertRowid, title, completed: 0 });
});
// 更新(Update)
app.put('/api/todos/:id', (req, res) => {
const { id } = req.params;
const { title, completed } = req.body;
db.prepare('UPDATE todos SET title = ?, completed = ? WHERE id = ?')
.run(title, completed, id);
res.json({ id: Number(id), title, completed });
});
// 削除(Delete)
app.delete('/api/todos/:id', (req, res) => {
const { id } = req.params;
db.prepare('DELETE FROM todos WHERE id = ?').run(id);
res.json({ success: true });
});
app.listen(3001, () => {
console.log('Server running on http://localhost:3001');
});
フロントエンド構築
セットアップ
cd ..
npm create vite@latest frontend -- --template react-ts
cd frontend
npm install
npm install @tanstack/react-query axios
src/api.ts
import axios from 'axios';
const api = axios.create({
baseURL: 'http://localhost:3001/api',
});
export type Todo = {
id: number;
title: string;
completed: number;
};
export const todoApi = {
getAll: () => api.get<Todo[]>('/todos').then(res => res.data),
create: (title: string) =>
api.post<Todo>('/todos', { title }).then(res => res.data),
update: (todo: Todo) =>
api.put<Todo>(`/todos/${todo.id}`, todo).then(res => res.data),
delete: (id: number) => api.delete(`/todos/${id}`),
};
src/App.tsx
import { useState } from 'react';
import {
useQuery,
useMutation,
useQueryClient,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query';
import { todoApi, Todo } from './api';
const queryClient = new QueryClient();
function TodoApp() {
const [newTitle, setNewTitle] = useState('');
const client = useQueryClient();
// 取得
const { data: todos = [], isLoading } = useQuery({
queryKey: ['todos'],
queryFn: todoApi.getAll,
});
// 作成
const createMutation = useMutation({
mutationFn: todoApi.create,
onSuccess: () => client.invalidateQueries({ queryKey: ['todos'] }),
});
// 更新(完了トグル)
const updateMutation = useMutation({
mutationFn: todoApi.update,
onSuccess: () => client.invalidateQueries({ queryKey: ['todos'] }),
});
// 削除
const deleteMutation = useMutation({
mutationFn: todoApi.delete,
onSuccess: () => client.invalidateQueries({ queryKey: ['todos'] }),
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!newTitle.trim()) return;
createMutation.mutate(newTitle);
setNewTitle('');
};
const toggleComplete = (todo: Todo) => {
updateMutation.mutate({
...todo,
completed: todo.completed ? 0 : 1,
});
};
if (isLoading) return <p>Loading...</p>;
return (
<div style={{ padding: '20px', maxWidth: '500px', margin: '0 auto' }}>
<h1>Todo App</h1>
<form onSubmit={handleSubmit}>
<input
type="text"
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
placeholder="新しいTodo"
/>
<button type="submit">追加</button>
</form>
<ul style={{ listStyle: 'none', padding: 0 }}>
{todos.map((todo) => (
<li key={todo.id}>
<input
type="checkbox"
checked={!!todo.completed}
onChange={() => toggleComplete(todo)}
/>
<span
style={{
textDecoration: todo.completed ? 'line-through' : 'none',
}}
>
{todo.title}
</span>
<button onClick={() => deleteMutation.mutate(todo.id)}>
削除
</button>
</li>
))}
</ul>
</div>
);
}
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<TodoApp />
</QueryClientProvider>
);
}
動作確認
# ターミナル1(バックエンド)
cd backend && node index.js
# ターミナル2(フロントエンド)
cd frontend && npm run dev
http://localhost:5173 でTodoアプリが動作します。
npm install @neondatabase/serverless をインストールし、better-sqlite3 の代わりに Neon クライアントを使用します。フロントエンド側のコードは変更不要です。
サーバー状態管理(TanStack Query)
サーバー状態とは
サーバー状態とは、APIから取得するデータのことです。
クライアント状態: UIの開閉、フォーム入力、テーマ設定
サーバー状態: ユーザー情報、商品一覧、投稿データ
なぜ専用ライブラリが必要なのか
useState + useEffect で毎回データ取得を書くと、以下の問題が発生します。
キャッシュ・重複排除・リトライが自動
loading, error, dataを毎回手書き
useState + useEffect の場合
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch('/api/users')
.then(res => res.json())
.then(data => setUsers(data))
.catch(err => setError(err))
.finally(() => setLoading(false));
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error!</p>;
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
TanStack Query の場合
function UserList() {
const { data: users, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(res => res.json()),
});
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error!</p>;
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
TanStack Queryが自動で提供する機能
| 機能 | 説明 |
|---|---|
| キャッシュ | 同じqueryKeyなら再取得せずキャッシュを返す |
| 重複排除 | 同時に同じリクエストが走らない |
| バックグラウンド更新 | タブ復帰時に自動で最新化 |
| リトライ | 失敗時に自動リトライ |
| ステータス管理 | loading, error, successを自動追跡 |
| ページネーション | 無限スクロール対応 |
キャッシュの共有
// ページA
const { data } = useQuery({ queryKey: ['user', 1], queryFn: fetchUser });
// ページB(同じデータを使う)
const { data } = useQuery({ queryKey: ['user', 1], queryFn: fetchUser });
// → APIを呼ばず、キャッシュから即座に返す
更新との連携
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: createUser,
onSuccess: () => {
// ユーザー一覧のキャッシュを無効化 → 自動再取得
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
Next.js App RouterでのTanStack Query
Next.js App Routerでは、Server ComponentsやServer Actionsに似た機能が組み込まれたため、TanStack Queryは必須ではなくなりました。
| パターン | 推奨 |
|---|---|
| 静的/SSRデータ | Server Components + fetch |
| リアルタイム更新 | TanStack Query |
| ポーリング | TanStack Query |
| 楽観的更新 | TanStack Query |
| 無限スクロール | TanStack Query |
| クライアント主体のSPA的な画面 | TanStack Query |
Pages Routerでは TanStack Query が定番でした。App Router では基本は Server Components を使い、クライアントで複雑な制御が必要な場合に TanStack Query を併用します。
Next.jsからReactへの移行手順
主な変更点
| 項目 | Next.js | React (Vite) |
|---|---|---|
| ビルド | next build | vite build |
| ルーティング | ファイルベース | React Router |
| API | API Routes / Server Actions | 別サーバー必要 |
| 画像 | next/image | img / 別ライブラリ |
| リンク | next/link | react-router-dom |
| 環境変数 | NEXT_PUBLIC_ | VITE_ |
1. 新しいReactプロジェクトの作成
npm create vite@latest my-app-react -- --template react-ts
cd my-app-react
npm install react-router-dom @tanstack/react-query axios
2. ルーティングの移行
Next.jsのファイルベースルーティングを React Router に置き換えます。
Next.js:
app/
├── page.tsx → /
├── about/page.tsx → /about
└── users/[id]/page.tsx → /users/:id
// src/router.tsx(React Router)
import { createBrowserRouter } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
import UserDetail from './pages/UserDetail';
export const router = createBrowserRouter([
{ path: '/', element: <Home /> },
{ path: '/about', element: <About /> },
{ path: '/users/:id', element: <UserDetail /> },
]);
// src/main.tsx
import { RouterProvider } from 'react-router-dom';
import { router } from './router';
ReactDOM.createRoot(document.getElementById('root')!).render(
<RouterProvider router={router} />
);
3. リンクの置き換え
// Before: Next.js
import Link from 'next/link';
<Link href="/about">About</Link>
// After: React Router
import { Link } from 'react-router-dom';
<Link to="/about">About</Link>
4. 動的ルートパラメータ
// Before: Next.js
export default function UserPage({ params }: { params: { id: string } }) {
const { id } = params;
}
// After: React Router
import { useParams } from 'react-router-dom';
export default function UserDetail() {
const { id } = useParams<{ id: string }>();
}
5. ナビゲーション(プログラム遷移)
// Before: Next.js
import { useRouter } from 'next/navigation';
const router = useRouter();
router.push('/dashboard');
// After: React Router
import { useNavigate } from 'react-router-dom';
const navigate = useNavigate();
navigate('/dashboard');
6. 画像の置き換え
// Before: Next.js
import Image from 'next/image';
<Image src="/logo.png" alt="Logo" width={100} height={50} />
// After: React
<img src="/logo.png" alt="Logo" width={100} height={50} />
7. API Routesの分離
Next.jsのAPI RoutesやServer Actionsは、Express等の別サーバーに移行が必要です。
// Before: Next.js(app/api/users/route.ts)
export async function GET() {
const users = await db.query('SELECT * FROM users');
return Response.json(users);
}
// After: Express(backend/index.js)
app.get('/api/users', async (req, res) => {
const users = await db.query('SELECT * FROM users');
res.json(users);
});
8. データ取得の変更
// Before: Next.js Server Component
async function UsersPage() {
const users = await fetch('/api/users').then(r => r.json());
return <UserList users={users} />;
}
// After: React + TanStack Query
function UsersPage() {
const { data: users, isLoading } = useQuery({
queryKey: ['users'],
queryFn: () =>
fetch('http://localhost:3001/api/users').then(r => r.json()),
});
if (isLoading) return <p>Loading...</p>;
return <UserList users={users} />;
}
9. 環境変数
# Before: Next.js(.env.local)
NEXT_PUBLIC_API_URL=http://localhost:3001
# After: Vite(.env)
VITE_API_URL=http://localhost:3001
// Before
process.env.NEXT_PUBLIC_API_URL
// After
import.meta.env.VITE_API_URL
10. メタデータ / Head
npm install react-helmet-async
// Before: Next.js
export const metadata = { title: 'Home' };
// After: React
import { Helmet } from 'react-helmet-async';
function Home() {
return (
<>
<Helmet>
<title>Home</title>
</Helmet>
<div>...</div>
</>
);
}
フォルダ構成の対応
Next.js React (Vite)
─────────────────────────────────────────
app/ → src/pages/
app/layout.tsx → src/App.tsx
app/api/ → backend/ (別プロジェクト)
components/ → src/components/
lib/ → src/lib/
public/ → public/
移行チェックリスト
- Viteプロジェクト作成
- パッケージインストール(react-router-dom, tanstack-query等)
- ルーティング設定(router.tsx)
- ページコンポーネント移行(app/ → src/pages/)
- 共通コンポーネント移行(components/)
- Link → react-router-dom Link
- useRouter → useNavigate
- params → useParams
- next/image → img
- API Routes → 別サーバー(Express等)
- Server Components → useQuery
- Server Actions → useMutation + API
- 環境変数 NEXT_PUBLIC_ → VITE_
- metadata → react-helmet-async
- スタイル移行(globals.css等)
最大の作業はAPI Routes / Server Actionsの分離です。バックエンドを別で立てる必要があります。コンポーネント自体はほぼそのまま移行できます。
参考文献
まとめ
ReactとNext.jsの選定は、プロジェクトの要件によって決まります。
| 選定基準 | React | Next.js |
|---|---|---|
| SEO重要 | 工夫が必要 | SSR/SSGで対応 |
| 管理画面/社内ツール | 最適 | 過剰になりがち |
| バックエンドが別にある | 最適 | 不要な機能が多い |
| フルスタックで完結したい | 別サーバーが必要 | 最適 |
| シンプルに保ちたい | 最適 | 機能が多い |
React単体で開発する場合は、Vite + React Router + TanStack Query + Zustand の組み合わせがスタンダードな構成です。バックエンドにはExpressやHonoを別プロジェクトとして構築し、API経由でデータをやり取りします。