【React】プロジェクト構築からCRUD実装・Next.js移行まで完全ガイド

PUBLISHED 2026-02-06

Reactは、Metaが開発したコンポーネントベースのUIライブラリです。Next.jsのようなフルスタックフレームワークとは異なり、UI構築に特化しているため、シンプルで柔軟な開発が可能です。

この記事では、ReactとNext.jsの違いからプロジェクト構築、DB連携によるCRUD実装、サーバー状態管理、そしてNext.jsからの移行手順までを体系的に解説します。

ReactとNext.jsの違い

ReactとNext.jsは「ライブラリ vs フレームワーク」という根本的な違いがあります。

項目ReactNext.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でプロジェクト作成

1
プロジェクト作成
npm create vite@latest my-app -- --template react-ts

create-react-app は非推奨(メンテナンス終了)のため、Viteを使用します。

2
依存パッケージのインストール
cd my-app
npm install
3
開発サーバーの起動
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アプリが動作します。

💡 Neonに置き換える場合

npm install @neondatabase/serverless をインストールし、better-sqlite3 の代わりに Neon クライアントを使用します。フロントエンド側のコードは変更不要です。

サーバー状態管理(TanStack Query)

サーバー状態とは

サーバー状態とは、APIから取得するデータのことです。

クライアント状態: UIの開閉、フォーム入力、テーマ設定
サーバー状態:    ユーザー情報、商品一覧、投稿データ

なぜ専用ライブラリが必要なのか

useState + useEffect で毎回データ取得を書くと、以下の問題が発生します。

✅ TanStack Query

キャッシュ・重複排除・リトライが自動

❌ 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.jsReact (Vite)
ビルドnext buildvite build
ルーティングファイルベースReact Router
APIAPI Routes / Server Actions別サーバー必要
画像next/imageimg / 別ライブラリ
リンクnext/linkreact-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/

移行チェックリスト

Next.js → React 移行チェックリスト
  • 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の選定は、プロジェクトの要件によって決まります。

選定基準ReactNext.js
SEO重要工夫が必要SSR/SSGで対応
管理画面/社内ツール最適過剰になりがち
バックエンドが別にある最適不要な機能が多い
フルスタックで完結したい別サーバーが必要最適
シンプルに保ちたい最適機能が多い

React単体で開発する場合は、Vite + React Router + TanStack Query + Zustand の組み合わせがスタンダードな構成です。バックエンドにはExpressやHonoを別プロジェクトとして構築し、API経由でデータをやり取りします。

CATEGORY
円