React/Next.js学習の正しい順番2026:挫折しない段階的アプローチ


はじめに:React/Next.js学習で挫折する本当の原因

React/Next.jsの学習で挫折する最大の原因は、学習順序の間違いです。

よくある失敗パターンを示します。

  • HTML/CSSの基本を理解していないのにReactを始める
  • JavaScriptの基礎が不十分なままJSXを書こうとする
  • React Hooksを理解する前にNext.jsのApp Routerに手を出す
  • 状態管理ライブラリ(Redux等)を最初から導入しようとする

この記事では、完全な初心者がNext.jsで実用的なWebアプリケーションを作れるようになるまでの正しい学習順序を、各段階のミニプロジェクト付きで解説します。


学習ロードマップの全体像

以下の7つのステップを順番に進めます。前のステップを飛ばさないことが重要です。

ステップ内容目安期間前提知識
Step 1HTML/CSSの基本2-3週間なし
Step 2JavaScriptの基礎3-4週間Step 1
Step 3React基礎(コンポーネント)2-3週間Step 2
Step 4React Hooks2-3週間Step 3
Step 5状態管理とデータ取得2-3週間Step 4
Step 6Next.js App Router3-4週間Step 5
Step 7実プロジェクト構築4-6週間Step 6

合計: 約18-26週間(4.5-6.5ヶ月)


Step 1: HTML/CSSの基本(2-3週間)

なぜReactの前にHTML/CSSが必要なのか

Reactは最終的にHTMLを生成します。ReactのJSXはHTMLの構文をベースにしているため、HTMLを理解していないとJSXも理解できません。

学習すべき項目

<!-- セマンティックHTML -->
<main>
  <article>
    <header>
      <h1>記事タイトル</h1>
      <time datetime="2026-03-06">2026年3月6日</time>
    </header>
    <p>記事の本文です。</p>
    <footer>
      <p>著者: 田中太郎</p>
    </footer>
  </article>
</main>

<!-- フォーム要素 -->
<form>
  <div>
    <label for="email">メールアドレス</label>
    <input type="email" id="email" name="email" required />
  </div>
  <div>
    <label for="password">パスワード</label>
    <input type="password" id="password" name="password" minlength="8" required />
  </div>
  <button type="submit">ログイン</button>
</form>

CSSで重点的に学ぶべきこと

/* Flexbox: 1次元レイアウト */
.navbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem 2rem;
  background-color: #1e293b;
  color: #f8fafc;
}

.nav-links {
  display: flex;
  gap: 1.5rem;
  list-style: none;
}

/* CSS Grid: 2次元レイアウト */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 1.5rem;
  padding: 2rem;
}

/* レスポンシブデザイン */
@media (max-width: 768px) {
  .navbar {
    flex-direction: column;
    gap: 1rem;
  }

  .card-grid {
    grid-template-columns: 1fr;
  }
}

/* CSS変数(カスタムプロパティ) */
:root {
  --color-primary: #3b82f6;
  --color-primary-hover: #2563eb;
  --radius: 0.5rem;
  --shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

.button {
  background-color: var(--color-primary);
  color: white;
  padding: 0.75rem 1.5rem;
  border: none;
  border-radius: var(--radius);
  cursor: pointer;
  transition: background-color 0.2s;
}

.button:hover {
  background-color: var(--color-primary-hover);
}

ミニプロジェクト: プロフィールカード

HTMLとCSSだけで、レスポンシブなプロフィールカードを作成してください。

要件:

  • プロフィール画像(丸形)
  • 名前、肩書き、自己紹介文
  • SNSリンクアイコン
  • ダークモード対応
  • モバイルでも崩れないレイアウト

Step 2: JavaScriptの基礎(3-4週間)

Reactに必要なJavaScript知識

Reactを始める前に、以下のJavaScript機能を確実に理解する必要があります。

// 1. アロー関数
const greet = (name) => `こんにちは、${name}さん`;

// 2. 分割代入(Reactのpropsで頻繁に使用)
const user = { name: '田中', age: 30, role: 'developer' };
const { name, age } = user;

const numbers = [1, 2, 3, 4, 5];
const [first, second, ...rest] = numbers;

// 3. スプレッド構文(Reactの状態更新で必須)
const updatedUser = { ...user, age: 31 };
const newArray = [...numbers, 6];

// 4. テンプレートリテラル
const message = `${name}さんは${age}歳です`;

// 5. 配列メソッド(map, filter, reduce)
// Reactのリスト表示で必須
const items = [
  { id: 1, name: 'りんご', price: 150, inStock: true },
  { id: 2, name: 'バナナ', price: 100, inStock: true },
  { id: 3, name: 'みかん', price: 200, inStock: false }
];

// mapで変換
const itemNames = items.map(item => item.name);
// ['りんご', 'バナナ', 'みかん']

// filterで絞り込み
const inStockItems = items.filter(item => item.inStock);
// [{ id: 1, ... }, { id: 2, ... }]

// reduceで集計
const totalPrice = items
  .filter(item => item.inStock)
  .reduce((sum, item) => sum + item.price, 0);
// 250

// 6. Promiseとasync/await
async function fetchPosts() {
  try {
    const response = await fetch('https://jsonplaceholder.typicode.com/posts');
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    const posts = await response.json();
    return posts;
  } catch (error) {
    console.error('取得エラー:', error);
    return [];
  }
}

// 7. モジュール(import/export)
// utils.js
export function formatDate(date) {
  return new Date(date).toLocaleDateString('ja-JP');
}

export function formatCurrency(amount) {
  return new Intl.NumberFormat('ja-JP', {
    style: 'currency',
    currency: 'JPY'
  }).format(amount);
}

ミニプロジェクト: Todoリスト(Vanilla JS版)

ReactなしのJavaScriptでTodoリストを作成します。この経験があると、Reactの便利さが理解できます。

// todo-app.js
class TodoApp {
  constructor() {
    this.todos = JSON.parse(localStorage.getItem('todos') || '[]');
    this.filter = 'all'; // all, active, completed
    this.init();
  }

  init() {
    this.form = document.getElementById('todo-form');
    this.input = document.getElementById('todo-input');
    this.list = document.getElementById('todo-list');
    this.filterButtons = document.querySelectorAll('[data-filter]');

    this.form.addEventListener('submit', (e) => {
      e.preventDefault();
      this.addTodo(this.input.value);
      this.input.value = '';
    });

    this.filterButtons.forEach(btn => {
      btn.addEventListener('click', () => {
        this.filter = btn.dataset.filter;
        this.updateFilterButtons();
        this.render();
      });
    });

    this.render();
  }

  addTodo(text) {
    if (!text.trim()) return;
    this.todos.push({
      id: Date.now(),
      text: text.trim(),
      completed: false
    });
    this.save();
    this.render();
  }

  toggleTodo(id) {
    const todo = this.todos.find(t => t.id === id);
    if (todo) {
      todo.completed = !todo.completed;
      this.save();
      this.render();
    }
  }

  deleteTodo(id) {
    this.todos = this.todos.filter(t => t.id !== id);
    this.save();
    this.render();
  }

  getFilteredTodos() {
    switch (this.filter) {
      case 'active':
        return this.todos.filter(t => !t.completed);
      case 'completed':
        return this.todos.filter(t => t.completed);
      default:
        return this.todos;
    }
  }

  save() {
    localStorage.setItem('todos', JSON.stringify(this.todos));
  }

  updateFilterButtons() {
    this.filterButtons.forEach(btn => {
      btn.classList.toggle('active', btn.dataset.filter === this.filter);
    });
  }

  render() {
    const filtered = this.getFilteredTodos();
    this.list.innerHTML = filtered.map(todo => `
      <li class="todo-item ${todo.completed ? 'completed' : ''}">
        <input
          type="checkbox"
          ${todo.completed ? 'checked' : ''}
          onchange="app.toggleTodo(${todo.id})"
        />
        <span>${todo.text}</span>
        <button onclick="app.deleteTodo(${todo.id})">x</button>
      </li>
    `).join('');

    const remaining = this.todos.filter(t => !t.completed).length;
    document.getElementById('todo-count').textContent =
      `${remaining}件の未完了タスク`;
  }
}

const app = new TodoApp();

このVanilla JS版を作った後にReact版を作ると、Reactが解決する問題(状態管理の複雑さ、DOM操作の手動管理)が体感できます。


Step 3: React基礎 — コンポーネント(2-3週間)

Reactの環境構築

## Vite + React + TypeScriptのプロジェクト作成
npm create vite@latest my-react-app -- --template react-ts
cd my-react-app
npm install
npm run dev

参照: Vite公式ドキュメント(https://vitejs.dev/guide/)では、最新のプロジェクト作成方法を確認できます。

コンポーネントの基本

// Button.tsx - 最もシンプルなコンポーネント
interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: 'primary' | 'secondary' | 'danger';
  disabled?: boolean;
}

function Button({ label, onClick, variant = 'primary', disabled = false }: ButtonProps) {
  const baseStyles = 'px-4 py-2 rounded font-medium transition-colors';
  const variantStyles = {
    primary: 'bg-blue-600 text-white hover:bg-blue-700',
    secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
    danger: 'bg-red-600 text-white hover:bg-red-700'
  };

  return (
    <button
      className={`${baseStyles} ${variantStyles[variant]}`}
      onClick={onClick}
      disabled={disabled}
    >
      {label}
    </button>
  );
}

export default Button;

propsとchildren

// Card.tsx - childrenを使ったコンポーネント
interface CardProps {
  title: string;
  children: React.ReactNode;
  footer?: React.ReactNode;
}

function Card({ title, children, footer }: CardProps) {
  return (
    <div className="border rounded-lg shadow-sm overflow-hidden">
      <div className="px-6 py-4 border-b bg-gray-50">
        <h3 className="text-lg font-semibold">{title}</h3>
      </div>
      <div className="px-6 py-4">
        {children}
      </div>
      {footer && (
        <div className="px-6 py-3 border-t bg-gray-50">
          {footer}
        </div>
      )}
    </div>
  );
}

// 使用例
function App() {
  return (
    <Card
      title="ユーザー情報"
      footer={<Button label="編集" onClick={() => console.log('edit')} />}
    >
      <p>名前: 田中太郎</p>
      <p>メール: tanaka@example.com</p>
    </Card>
  );
}

コンポーネントのリスト表示

// UserList.tsx
interface User {
  id: number;
  name: string;
  email: string;
  role: string;
}

interface UserListProps {
  users: User[];
  onSelectUser: (user: User) => void;
}

function UserList({ users, onSelectUser }: UserListProps) {
  if (users.length === 0) {
    return <p className="text-gray-500">ユーザーが見つかりません</p>;
  }

  return (
    <ul className="divide-y">
      {users.map(user => (
        <li
          key={user.id}
          className="py-3 px-4 hover:bg-gray-50 cursor-pointer"
          onClick={() => onSelectUser(user)}
        >
          <div className="font-medium">{user.name}</div>
          <div className="text-sm text-gray-500">{user.email}</div>
          <span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
            {user.role}
          </span>
        </li>
      ))}
    </ul>
  );
}

注意: key propにはリスト内で一意な値を設定してください。配列のインデックスをkeyにするのはアンチパターンです(参照: React公式ドキュメント https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key)。

ミニプロジェクト: プロフィールカード一覧

React版のプロフィールカード一覧を作成してください。

// ProfileCard.tsx
interface Profile {
  id: number;
  name: string;
  title: string;
  bio: string;
  skills: string[];
  avatarUrl: string;
}

function ProfileCard({ name, title, bio, skills, avatarUrl }: Profile) {
  return (
    <div className="bg-white rounded-lg shadow-md p-6 max-w-sm">
      <div className="flex items-center gap-4 mb-4">
        <img
          src={avatarUrl}
          alt={`${name}のアバター`}
          className="w-16 h-16 rounded-full object-cover"
        />
        <div>
          <h3 className="font-bold text-lg">{name}</h3>
          <p className="text-gray-500 text-sm">{title}</p>
        </div>
      </div>
      <p className="text-gray-700 mb-4">{bio}</p>
      <div className="flex flex-wrap gap-2">
        {skills.map(skill => (
          <span
            key={skill}
            className="bg-blue-100 text-blue-800 text-xs px-3 py-1 rounded-full"
          >
            {skill}
          </span>
        ))}
      </div>
    </div>
  );
}

// App.tsx
function App() {
  const profiles: Profile[] = [
    {
      id: 1,
      name: '田中太郎',
      title: 'フロントエンドエンジニア',
      bio: 'React/TypeScript歴3年。UIの設計が得意です。',
      skills: ['React', 'TypeScript', 'Next.js', 'Tailwind CSS'],
      avatarUrl: 'https://api.dicebear.com/7.x/avataaars/svg?seed=tanaka'
    },
    {
      id: 2,
      name: '佐藤花子',
      title: 'フルスタックエンジニア',
      bio: 'バックエンドからフロントまで幅広く対応します。',
      skills: ['Node.js', 'React', 'PostgreSQL', 'Docker'],
      avatarUrl: 'https://api.dicebear.com/7.x/avataaars/svg?seed=sato'
    }
  ];

  return (
    <div className="min-h-screen bg-gray-100 p-8">
      <h1 className="text-2xl font-bold mb-6">チームメンバー</h1>
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        {profiles.map(profile => (
          <ProfileCard key={profile.id} {...profile} />
        ))}
      </div>
    </div>
  );
}

Step 4: React Hooks(2-3週間)

useState — 状態管理の基本

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>カウント: {count}</p>
      <button onClick={() => setCount(prev => prev + 1)}>+1</button>
      <button onClick={() => setCount(prev => prev - 1)}>-1</button>
      <button onClick={() => setCount(0)}>リセット</button>
    </div>
  );
}

// オブジェクトの状態管理
interface FormData {
  name: string;
  email: string;
  message: string;
}

function ContactForm() {
  const [formData, setFormData] = useState<FormData>({
    name: '',
    email: '',
    message: ''
  });

  const [isSubmitting, setIsSubmitting] = useState(false);
  const [submitResult, setSubmitResult] = useState<string | null>(null);

  const handleChange = (
    e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
  ) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
  };

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setIsSubmitting(true);
    setSubmitResult(null);

    try {
      const response = await fetch('/api/contact', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(formData)
      });

      if (response.ok) {
        setSubmitResult('送信が完了しました');
        setFormData({ name: '', email: '', message: '' });
      } else {
        setSubmitResult('送信に失敗しました');
      }
    } catch {
      setSubmitResult('通信エラーが発生しました');
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="name">名前</label>
        <input
          id="name"
          name="name"
          value={formData.name}
          onChange={handleChange}
          required
        />
      </div>
      <div>
        <label htmlFor="email">メール</label>
        <input
          id="email"
          name="email"
          type="email"
          value={formData.email}
          onChange={handleChange}
          required
        />
      </div>
      <div>
        <label htmlFor="message">メッセージ</label>
        <textarea
          id="message"
          name="message"
          value={formData.message}
          onChange={handleChange}
          required
        />
      </div>
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? '送信中...' : '送信'}
      </button>
      {submitResult && <p>{submitResult}</p>}
    </form>
  );
}

useEffect — 副作用の管理

import { useState, useEffect } from 'react';

interface Post {
  id: number;
  title: string;
  body: string;
  userId: number;
}

function PostList() {
  const [posts, setPosts] = useState<Post[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [page, setPage] = useState(1);

  useEffect(() => {
    let isMounted = true;

    async function fetchPosts() {
      setLoading(true);
      setError(null);

      try {
        const response = await fetch(
          `https://jsonplaceholder.typicode.com/posts?_page=${page}&_limit=10`
        );

        if (!response.ok) {
          throw new Error('データの取得に失敗しました');
        }

        const data: Post[] = await response.json();

        if (isMounted) {
          setPosts(data);
        }
      } catch (err) {
        if (isMounted) {
          setError(err instanceof Error ? err.message : '不明なエラー');
        }
      } finally {
        if (isMounted) {
          setLoading(false);
        }
      }
    }

    fetchPosts();

    // クリーンアップ関数
    return () => {
      isMounted = false;
    };
  }, [page]); // pageが変更されたときに再実行

  if (loading) return <div>読み込み中...</div>;
  if (error) return <div>エラー: {error}</div>;

  return (
    <div>
      <h2>投稿一覧(ページ {page})</h2>
      <ul>
        {posts.map(post => (
          <li key={post.id}>
            <h3>{post.title}</h3>
            <p>{post.body.substring(0, 100)}...</p>
          </li>
        ))}
      </ul>
      <div>
        <button
          onClick={() => setPage(p => Math.max(1, p - 1))}
          disabled={page === 1}
        >
          前のページ
        </button>
        <span>ページ {page}</span>
        <button onClick={() => setPage(p => p + 1)}>
          次のページ
        </button>
      </div>
    </div>
  );
}

カスタムフック

// useLocalStorage.ts - localStorageと同期するフック
function useLocalStorage<T>(key: string, initialValue: T) {
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setValue = (value: T | ((prev: T) => T)) => {
    const valueToStore = value instanceof Function ? value(storedValue) : value;
    setStoredValue(valueToStore);
    localStorage.setItem(key, JSON.stringify(valueToStore));
  };

  return [storedValue, setValue] as const;
}

// useDebounce.ts - 入力のデバウンス
function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

// 使用例: 検索フォーム
function SearchForm() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 300);
  const [results, setResults] = useState<string[]>([]);

  useEffect(() => {
    if (!debouncedQuery) {
      setResults([]);
      return;
    }

    async function search() {
      const response = await fetch(`/api/search?q=${encodeURIComponent(debouncedQuery)}`);
      const data = await response.json();
      setResults(data);
    }

    search();
  }, [debouncedQuery]);

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="検索..."
      />
      <ul>
        {results.map((result, index) => (
          <li key={index}>{result}</li>
        ))}
      </ul>
    </div>
  );
}

ミニプロジェクト: Todoアプリ(React版)

Step 2で作成したVanilla JS版のTodoアプリをReact + TypeScriptで書き直してください。

// useTodos.ts - カスタムフック
interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

type Filter = 'all' | 'active' | 'completed';

function useTodos() {
  const [todos, setTodos] = useLocalStorage<Todo[]>('todos', []);
  const [filter, setFilter] = useState<Filter>('all');

  const addTodo = (text: string) => {
    if (!text.trim()) return;
    setTodos(prev => [
      ...prev,
      { id: Date.now(), text: text.trim(), completed: false }
    ]);
  };

  const toggleTodo = (id: number) => {
    setTodos(prev =>
      prev.map(todo =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };

  const deleteTodo = (id: number) => {
    setTodos(prev => prev.filter(todo => todo.id !== id));
  };

  const filteredTodos = todos.filter(todo => {
    if (filter === 'active') return !todo.completed;
    if (filter === 'completed') return todo.completed;
    return true;
  });

  const remaining = todos.filter(t => !t.completed).length;

  return {
    todos: filteredTodos,
    remaining,
    filter,
    setFilter,
    addTodo,
    toggleTodo,
    deleteTodo
  };
}

Step 5: 状態管理とデータ取得(2-3週間)

状態管理の選択肢

2026年時点でのReact状態管理ライブラリの比較を示します。

ライブラリ学習コストバンドルサイズ特徴
React Context0KB(組み込み)小規模アプリに最適
Zustand1KBシンプルで軽量。中規模以上推奨
Jotai2KBアトミック。細かい状態管理向き
Redux Toolkit11KB大規模向け。学習コスト高め

初心者にはZustandを推奨します。APIがシンプルで直感的です。

// store.ts - Zustandでの状態管理
import { create } from 'zustand';

interface CartItem {
  id: number;
  name: string;
  price: number;
  quantity: number;
}

interface CartStore {
  items: CartItem[];
  addItem: (item: Omit<CartItem, 'quantity'>) => void;
  removeItem: (id: number) => void;
  updateQuantity: (id: number, quantity: number) => void;
  clearCart: () => void;
  totalPrice: () => number;
  totalItems: () => number;
}

const useCartStore = create<CartStore>((set, get) => ({
  items: [],

  addItem: (item) => set(state => {
    const existing = state.items.find(i => i.id === item.id);
    if (existing) {
      return {
        items: state.items.map(i =>
          i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
        )
      };
    }
    return { items: [...state.items, { ...item, quantity: 1 }] };
  }),

  removeItem: (id) => set(state => ({
    items: state.items.filter(i => i.id !== id)
  })),

  updateQuantity: (id, quantity) => set(state => ({
    items: quantity > 0
      ? state.items.map(i => i.id === id ? { ...i, quantity } : i)
      : state.items.filter(i => i.id !== id)
  })),

  clearCart: () => set({ items: [] }),

  totalPrice: () => get().items.reduce(
    (sum, item) => sum + item.price * item.quantity, 0
  ),

  totalItems: () => get().items.reduce(
    (sum, item) => sum + item.quantity, 0
  )
}));

// CartComponent.tsx
function Cart() {
  const { items, removeItem, updateQuantity, clearCart, totalPrice, totalItems } = useCartStore();

  return (
    <div>
      <h2>カート({totalItems()}件)</h2>
      {items.length === 0 ? (
        <p>カートは空です</p>
      ) : (
        <>
          {items.map(item => (
            <div key={item.id} className="flex items-center gap-4 py-2">
              <span>{item.name}</span>
              <span>{item.price.toLocaleString()}円</span>
              <input
                type="number"
                min="0"
                value={item.quantity}
                onChange={(e) => updateQuantity(item.id, parseInt(e.target.value) || 0)}
                className="w-16 border rounded px-2"
              />
              <button onClick={() => removeItem(item.id)}>削除</button>
            </div>
          ))}
          <div className="mt-4 pt-4 border-t">
            <p className="text-lg font-bold">合計: {totalPrice().toLocaleString()}円</p>
            <button onClick={clearCart}>カートを空にする</button>
          </div>
        </>
      )}
    </div>
  );
}

参照: Zustand公式ドキュメント(https://docs.pmnd.rs/zustand/getting-started/introduction)


Step 6: Next.js App Router(3-4週間)

Next.jsの環境構築

npx create-next-app@latest my-next-app --typescript --tailwind --app --src-dir
cd my-next-app
npm run dev

参照: Next.js公式ドキュメント(https://nextjs.org/docs/getting-started/installation)

App Routerの基本構造

src/
  app/
    layout.tsx          # ルートレイアウト
    page.tsx            # / のページ
    loading.tsx         # ローディングUI
    error.tsx           # エラーUI
    not-found.tsx       # 404ページ
    blog/
      page.tsx          # /blog のページ
      [slug]/
        page.tsx        # /blog/:slug のページ
    api/
      posts/
        route.ts        # API Route: /api/posts

Server Componentsとデータ取得

// app/blog/page.tsx - Server Component(デフォルト)
interface Post {
  id: number;
  title: string;
  excerpt: string;
  publishedAt: string;
}

async function getPosts(): Promise<Post[]> {
  const response = await fetch('https://api.example.com/posts', {
    next: { revalidate: 3600 } // 1時間キャッシュ
  });

  if (!response.ok) {
    throw new Error('投稿の取得に失敗しました');
  }

  return response.json();
}

export default async function BlogPage() {
  const posts = await getPosts();

  return (
    <main className="max-w-4xl mx-auto py-8 px-4">
      <h1 className="text-3xl font-bold mb-8">ブログ記事一覧</h1>
      <div className="space-y-6">
        {posts.map(post => (
          <article key={post.id} className="border rounded-lg p-6">
            <h2 className="text-xl font-semibold mb-2">
              <a href={`/blog/${post.id}`} className="hover:text-blue-600">
                {post.title}
              </a>
            </h2>
            <p className="text-gray-600 mb-2">{post.excerpt}</p>
            <time className="text-sm text-gray-400">
              {new Date(post.publishedAt).toLocaleDateString('ja-JP')}
            </time>
          </article>
        ))}
      </div>
    </main>
  );
}

// app/blog/[slug]/page.tsx - 動的ルート
interface BlogPostPageProps {
  params: Promise<{ slug: string }>;
}

async function getPost(slug: string) {
  const response = await fetch(`https://api.example.com/posts/${slug}`);
  if (!response.ok) return null;
  return response.json();
}

export default async function BlogPostPage({ params }: BlogPostPageProps) {
  const { slug } = await params;
  const post = await getPost(slug);

  if (!post) {
    return <div>記事が見つかりませんでした</div>;
  }

  return (
    <article className="max-w-3xl mx-auto py-8 px-4">
      <h1 className="text-3xl font-bold mb-4">{post.title}</h1>
      <div className="prose" dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

Client Components

// app/components/SearchBar.tsx
'use client'; // Client Componentであることを宣言

import { useState, useEffect } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';

export default function SearchBar() {
  const router = useRouter();
  const searchParams = useSearchParams();
  const [query, setQuery] = useState(searchParams.get('q') || '');

  const handleSearch = (e: React.FormEvent) => {
    e.preventDefault();
    if (query.trim()) {
      router.push(`/search?q=${encodeURIComponent(query.trim())}`);
    }
  };

  return (
    <form onSubmit={handleSearch} className="flex gap-2">
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="記事を検索..."
        className="flex-1 border rounded px-4 py-2"
      />
      <button type="submit" className="bg-blue-600 text-white px-6 py-2 rounded">
        検索
      </button>
    </form>
  );
}

API Routes

// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';

interface Post {
  id: number;
  title: string;
  content: string;
  createdAt: string;
}

const posts: Post[] = [
  { id: 1, title: '最初の投稿', content: 'こんにちは', createdAt: '2026-03-06' }
];

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const page = parseInt(searchParams.get('page') || '1');
  const limit = parseInt(searchParams.get('limit') || '10');

  const start = (page - 1) * limit;
  const paginatedPosts = posts.slice(start, start + limit);

  return NextResponse.json({
    data: paginatedPosts,
    pagination: {
      page,
      limit,
      total: posts.length,
      totalPages: Math.ceil(posts.length / limit)
    }
  });
}

export async function POST(request: NextRequest) {
  const body = await request.json();
  const { title, content } = body;

  if (!title || !content) {
    return NextResponse.json(
      { error: 'タイトルと本文は必須です' },
      { status: 400 }
    );
  }

  const newPost: Post = {
    id: posts.length + 1,
    title,
    content,
    createdAt: new Date().toISOString()
  };

  posts.push(newPost);

  return NextResponse.json(newPost, { status: 201 });
}

Step 7: 実プロジェクト構築(4-6週間)

総合プロジェクト: ブックマーク管理アプリ

Step 1-6で学んだ全技術を統合した実用的なアプリケーションを構築します。

機能要件
  • ブックマークの追加・編集・削除(CRUD)
  • タグによる分類と検索
  • OGP情報の自動取得
  • レスポンシブデザイン
  • ダークモード対応
技術スタック
レイヤー技術
フレームワークNext.js 15 (App Router)
言語TypeScript
スタイリングTailwind CSS
状態管理Zustand
DBSQLite (better-sqlite3)
テストVitest + Testing Library
ディレクトリ構成
src/
  app/
    layout.tsx
    page.tsx
    bookmarks/
      page.tsx
      [id]/
        page.tsx
    api/
      bookmarks/
        route.ts
        [id]/
          route.ts
  components/
    BookmarkCard.tsx
    BookmarkForm.tsx
    SearchBar.tsx
    TagFilter.tsx
  lib/
    db.ts
    ogp.ts
  types/
    index.ts

このプロジェクトを完成させることで、実務レベルのNext.jsアプリケーション構築スキルが身につきます。


学習リソースまとめ

リソースURL特徴
React公式ドキュメントhttps://react.dev/最も正確。チュートリアルが充実
Next.js公式ドキュメントhttps://nextjs.org/docsApp Routerの詳細が充実
MDN Web Docshttps://developer.mozilla.org/ja/HTML/CSS/JSのリファレンス
TypeScript Handbookhttps://www.typescriptlang.org/docs/TypeScriptの公式ガイド

まとめ

React/Next.js学習の成功の鍵は、正しい順序で段階的に学ぶことです。

  1. Step 1-2(HTML/CSS/JS)を飛ばさない — 基礎なしにReactは理解できない
  2. Step 3-4(React基礎/Hooks)で小さなコンポーネントを作る練習を積む
  3. Step 5(状態管理)は複雑なライブラリより、まずZustandのシンプルなAPIから
  4. Step 6(Next.js)はReactを十分に理解してから取り組む
  5. Step 7(実プロジェクト)で全スキルを統合して定着させる

各ステップでミニプロジェクトを作成し、実際に手を動かすことが最も重要です。理論だけでは身につきません。

プログラミングスクールを活用する場合は、React/Next.jsのカリキュラムが充実しており、現役エンジニアのメンターがいるスクールを選ぶことをおすすめします。特にポートフォリオ制作(Step 7)のサポートが手厚いスクールが効果的です。

🎨 プロクリエイターから学ぶオンライン動画講座【Coloso】
デザイン・イラスト・映像・3D・プログラミングなど、各分野のプロが直接教えるオンライン動画講座。韓国発グローバルプラットフォームで、実践的なスキルを身につけよう。
→ Colosoで講座を探す(無料会員登録)
---

関連記事