Contents
開発環境の準備とプロジェクト作成
このセクションでは、2026 年時点で「実務レベルの」React + TypeScript アプリをすぐに始められるように、必要なツールのインストールから雛形生成までの手順をまとめます。正しいバージョン情報は公式サイトで随時確認し、ローカル環境と CI 環境の両方で同一になることがトラブル防止につながります。
Node.js とパッケージマネージャー
Node.js は LTS 系列を利用することで長期サポートと安定性が保証されます。2026 年 7 月現在、公式サイト(https://nodejs.org/ja)で公開されている最新の LTS バージョンは v20.x 系 です。インストール後にバージョンを確認し、npm または Yarn が正しく動作することを確かめましょう。
|
1 2 3 4 5 6 7 8 |
# ダウンロードしたインストーラでインストール後 node -v # 例: v20.12.0 npm -v # 例: 10.8.1 # Yarn を使用したい場合 npm i -g yarn@latest yarn -v # 例: 4.2.2 |
ポイント:
nvm(Node Version Manager)を併用すると、プロジェクトごとに Node バージョンを切り替えられるため便利です。
Vite + React + TypeScript テンプレートの生成
Vite は開発サーバの高速起動と最適化ビルドが特徴で、公式テンプレートから React + TypeScript の雛形を即座に作成できます。以下のコマンドはプロジェクト名 my-todo を例にしています。
|
1 2 3 4 5 6 7 |
npm create vite@latest my-todo -- --template react-ts # あるいは Yarn を使う場合 yarn create vite my-todo --template react-ts cd my-todo npm install # または yarn |
生成直後に npm run dev(または yarn dev)を実行すると、http://localhost:5173 で Vite のデフォルトページが表示されます。
Tailwind CSS の導入
Tailwind CSS はユーティリティファーストの CSS フレームワークで、React コンポーネントと相性が良く、モダン UI 開発の事実上の標準となっています。2026 年時点では Tailwind CSS v3.x が LTS として提供されており、公式ドキュメント(https://tailwindcss.com/docs/installation)に従ってインストールします。
|
1 2 3 |
npm i -D tailwindcss@latest postcss autoprefixer npx tailwindcss init -p # tailwind.config.cjs と postcss.config.cjs が生成される |
tailwind.config.cjs に Vite のソースディレクトリ全体を対象にする設定を書き込みます。
|
1 2 3 4 5 6 7 8 9 |
/** @type {import('tailwindcss').Config} */ module.exports = { content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], theme: { extend: {}, }, plugins: [], }; |
次に src/index.css に Tailwind のベース・コンポーネント・ユーティリティをインポートし、エントリポイントで読み込むだけです。
|
1 2 3 4 5 |
/* src/index.css */ @tailwind base; @tailwind components; @tailwind utilities; |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
// src/main.tsx import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; import './index.css'; // ← ここで Tailwind を適用 ReactDOM.createRoot(document.getElementById('root')!).render( <React.StrictMode> <App /> </React.StrictMode>, ); |
まとめ:Node.js LTS → Vite + React/TS テンプレート → Tailwind CSS の順にセットアップすれば、2026 年版のフロントエンド開発環境が整います。次章ではこの土台上で利用できる最新の React API と TypeScript 活用法を見ていきます。
React 18/19 の基礎と TypeScript 活用
React 19 が正式にリリースされるかどうかは2026年時点でも未確定ですが、React 18(2022 年リリース)で導入された Concurrent Mode、useTransition、useId などの機能はすでに安定版として利用可能です。本章ではこれらの API の実装例と、TypeScript で安全に型付けするベストプラクティスを紹介します。
Concurrent Mode と useTransition
useTransition は UI 更新と副作用(データフェッチなど)を分離し、ユーザー操作がブロックされないようにするためのフックです。React 18 から利用でき、React 19 が出たとしても基本的な挙動は変わりません。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
import { useState, useTransition } from 'react'; function TodoInput() { const [text, setText] = useState(''); const [isPending, startTransition] = useTransition(); const addTodo = async () => { // 非同期処理は Transition 内で実行 startTransition(async () => { await fetch('/api/todos', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text }), }); }); setText(''); // UI は即座にクリア }; return ( <div className="flex gap-2"> <input value={text} onChange={e => setText(e.target.value)} placeholder="タスクを入力" className="border rounded p-1" /> <button onClick={addTodo} disabled={isPending} className="bg-blue-600 text-white px-3 rounded" > {isPending ? '保存中…' : '追加'} </button> </div> ); } |
ポイント:
startTransitionのコールバックは必ず非同期関数にし、エラーハンドリングは内部で行うかtry/catchで包みます。
一意な ID 生成 useId
SSR(サーバーサイドレンダリング)環境でも衝突しない一意の文字列を取得できるフックです。ラベルと入力要素の紐付けやアクセシビリティ向上に活用します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import { useId } from 'react'; function LabeledInput() { const id = useId(); // "react-:r0:" のような文字列が生成される return ( <div className="flex flex-col"> <label htmlFor={id}>タスク名</label> <input id={id} type="text" className="border rounded p-1" /> </div> ); } |
Props・State の型定義ベストプラクティス
| 項目 | 推奨手法 | 補足 |
|---|---|---|
| Props | インターフェースまたは type エイリアスで明示的に定義。必須とオプションは ? で分離 |
React.FC は暗黙の children が付与されるため、コンポーネントごとに型を自前で書く方が安全 |
| State | useReducer と discriminated union を組み合わせて状態遷移を型安全に管理 |
大規模なロジックは immer 等のミュータブルヘルパーと併用しても可 |
| ユーティリティ型 | Partial<T>, Pick<T, K> で部分的型取得、Record<string, unknown> で汎用オブジェクトを表現 |
API のリクエスト/レスポンスの差分管理に便利 |
Props の具体例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
interface TodoItemProps { id: string; title: string; completed?: boolean; // オプションは ? で明示 } export const TodoItem = ({ id, title, completed = false, }: TodoItemProps) => ( <li className={completed ? 'line-through' : ''} data-id={id}> {title} </li> ); |
State と Action の型安全な定義例
|
1 2 3 4 5 6 7 |
type Todo = { id: string; title: string; completed: boolean }; type TodoAction = | { type: 'add'; payload: Todo } | { type: 'toggle'; payload: { id: string } } | { type: 'remove'; payload: { id: string } }; |
まとめ:useTransition と useId は React 18 が提供する重要な Concurrent 機能です。これらを正しく使い、Props/State を TypeScript の型システムで厳密に定義すれば、開発中のバグ検出率が大幅に向上します。
Next.js App Router(13+)と Server Components / Server Actions
Next.js 13 系からは App Router がデフォルトとなり、app/ ディレクトリ配下でページ・レイアウトを宣言的に管理できます。さらに Server Component と Server Action を組み合わせることで、クライアント側の JavaScript バンドルサイズを削減しながらサーバーロジックを書けます。この章では正しいディレクトリ構成と、実装上の落とし穴('use server' の位置など)を踏まえたコード例をご紹介します。
ディレクトリ構成の指針
以下は Todo アプリに最小限必要なファイル構造です。コメントで役割を補足しています。
|
1 2 3 4 5 6 7 8 9 |
app/ ├─ layout.tsx # ルートレイアウト(Server Component) ├─ page.tsx # / のトップページ(Client Component が必要なら 'use client' を付与) └─ todo/ ├─ page.tsx # /todo の一覧表示(Server Component) └─ create/ ├─ page.tsx # Todo 作成フォーム(Client Component) └─ actions.ts # Server Action 定義ファイル(Server Component でエクスポート) |
- layout.tsx は全ページ共通の HTML 構造を提供し、
<html>・<body>タグを書き換える唯一の場所です。 - page.tsx が
app/の直下にある場合はデフォルトで Server Component になるため、クライアント側で状態管理が必要なときだけ'use client'を付けます。
Server Component の実装例
サーバーサイドでデータ取得し、HTML だけを返すシンプルなコンポーネントです。React の Suspense と相性が良く、初回ロードが高速になります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// app/todo/page.tsx (Server Component) import { fetchTodos } from '@/lib/api'; export default async function TodoList() { const todos = await fetchTodos(); // ← サーバー上で直接実行 return ( <section className="space-y-2"> <h1 className="text-xl font-bold">Todo 一覧</h1> <ul className="list-disc pl-5"> {todos.map(todo => ( <li key={todo.id} className={todo.completed ? 'line-through' : ''}> {todo.title} </li> ))} </ul> </section> ); } |
注意:
fetchTodosはサーバー専用のヘルパー(例:node-fetchまたは Next.js のfetch)で実装し、クライアント側に不要な依存を持ち込まないようにします。
正しい Server Action の書き方
Server Action は ファイルトップレベル にエクスポートされた関数として定義し、関数内部の最初行で 'use server' を宣言します。クライアント側コンポーネントからは通常通り import して呼び出すだけです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// app/todo/create/actions.ts (Server Action) export async function addTodo(formData: FormData) { 'use server'; // ← 必ず関数の先頭に置く const title = formData.get('title'); if (!title || typeof title !== 'string') { throw new Error('タイトルが無効です'); } await fetch(`${process.env.API_URL}/todos`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title }), }); } |
次に、クライアントコンポーネント側でこの Action を呼び出す例です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
// app/todo/create/page.tsx (Client Component) 'use client'; import { useState, FormEvent } from 'react'; import { addTodo } from './actions'; export default function CreateTodo() { const [title, setTitle] = useState(''); const handleSubmit = async (e: FormEvent) => { e.preventDefault(); const fd = new FormData(); fd.append('title', title); await addTodo(fd); // Server Action がサーバー側で実行される setTitle(''); // フィールドをリセット }; return ( <form onSubmit={handleSubmit} className="flex gap-2"> <input name="title" value={title} onChange={e => setTitle(e.target.value)} placeholder="新しいタスク" required className="border rounded p-1 flex-1" /> <button type="submit" className="bg-green-600 text-white px-4 rounded"> 追加 </button> </form> ); } |
重要:
'use client'が付いたファイルはクライアントバンドルに含まれ、そこでimport { addTodo } from './actions'としても サーバー側ロジックは切り離されて実行 されます。逆に Server Component に'use server'を書く必要はありません。
まとめ:App Router のディレクトリ規則と、Server Action を「トップレベル関数+'use server'」という形で記述すれば、クライアントコードはシンプルに保ちつつサーバーロジックを安全に呼び出せます。次章ではローカル UI 状態とサーバーデータ取得の統合パターンを見ていきましょう。
状態管理とデータフェッチング
Todo アプリは「ユーザー操作による即時状態」と「バックエンドから取得する永続データ」の二層構造が典型的です。本章では useReducer でローカル UI を管理し、 TanStack Query v5(旧 React Query)でサーバーデータのフェッチ・キャッシュを行う実装例を示します。両者を組み合わせることで、リアクティブかつスケーラブルな状態ツリーが構築できます。
useReducer でローカル UI を管理
useReducer は状態遷移が明確になるため、Todo の「完了/未完了」や「削除」など複数アクションが絡む場面に適しています。以下は型安全な reducer とその使用例です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// src/hooks/useTodoReducer.ts type Todo = { id: string; title: string; completed: boolean }; type Action = | { type: 'add'; payload: Todo } | { type: 'toggle'; payload: { id: string } } | { type: 'remove'; payload: { id: string } }; export function todoReducer(state: Todo[], action: Action): Todo[] { switch (action.type) { case 'add': return [...state, action.payload]; case 'toggle': return state.map(t => t.id === action.payload.id ? { ...t, completed: !t.completed } : t, ); case 'remove': return state.filter(t => t.id !== action.payload.id); default: return state; } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// src/components/LocalTodoList.tsx (Client Component) 'use client'; import { useReducer, useEffect } from 'react'; import { todoReducer } from '@/hooks/useTodoReducer'; export default function LocalTodoList({ initialTodos }: { initialTodos: Todo[] }) { const [todos, dispatch] = useReducer(todoReducer, initialTodos); // UI 上だけ完了状態をトグル const toggle = (id: string) => dispatch({ type: 'toggle', payload: { id } }); return ( <ul className="space-y-1"> {todos.map(t => ( <li key={t.id} className="flex items-center gap-2"> <input type="checkbox" checked={t.completed} onChange={() => toggle(t.id)} /> <span className={t.completed ? 'line-through' : ''}>{t.title}</span> </li> ))} </ul> ); } |
TanStack Query v5 の基本パターン
TanStack Query v5 は createQueryClient と useSuspenseQuery / useQuery を中心に構成され、React の Suspense と自然に連携できます。以下は Todo リストを取得し、キャッシュとリフェッチ戦略を設定した例です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// src/lib/queryClient.ts import { createQueryClient, QueryClientProvider } from '@tanstack/react-query'; export const queryClient = createQueryClient({ defaultOptions: { queries: { staleTime: 60_000, // 1 分間はキャッシュを有効化 retry: 2, refetchOnWindowFocus: false, }, }, }); export function QueryProvider({ children }: { children: React.ReactNode }) { return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>; } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// src/components/RemoteTodoList.tsx (Server Component でも使用可) import { useSuspenseQuery } from '@tanstack/react-query'; import { fetchTodos } from '@/lib/api'; export default function RemoteTodoList() { const { data: todos } = useSuspenseQuery(['todos'], fetchTodos); return ( <ul className="space-y-1"> {todos?.map(todo => ( <li key={todo.id} className={todo.completed ? 'line-through' : ''}> {todo.title} </li> ))} </ul> ); } |
補足:
fetchTodosはexport async function fetchTodos() { return await fetch(...).then(r => r.json()); }のようにサーバー専用のロジックで実装し、クライアント側には露出させません。
Reducer と Query を統合したハイブリッド例
サーバーから取得したデータをローカル useReducer に流し込み、完了フラグだけはクライアント側で即時に切り替えるパターンです。これにより 楽観的 UI 更新 が容易になります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
// src/components/IntegratedTodoList.tsx (Client Component) 'use client'; import { useSuspenseQuery } from '@tanstack/react-query'; import { fetchTodos } from '@/lib/api'; import { todoReducer } from '@/hooks/useTodoReducer'; export default function IntegratedTodoList() { const { data: serverTodos = [] } = useSuspenseQuery(['todos'], fetchTodos); const [localTodos, dispatch] = useReducer(todoReducer, serverTodos); // 完了トグルはローカルだけ更新し、バックエンドへ PATCH を非同期で送信 const toggle = async (id: string) => { dispatch({ type: 'toggle', payload: { id } }); await fetch(`${process.env.API_URL}/todos/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ completed: true }), }); }; return ( <ul className="space-y-2"> {localTodos.map(t => ( <li key={t.id} className="flex items-center gap-3"> <input type="checkbox" checked={t.completed} onChange={() => toggle(t.id)} /> <span className={t.completed ? 'line-through' : ''}>{t.title}</span> </li> ))} </ul> ); } |
まとめ:useReducer が UI の即時反応性を担い、TanStack Query がサーバーデータの取得・キャッシュ・再検証を統括します。両者を組み合わせることで「データは常に最新」「UI は瞬間的に変化」する理想的な Todo アプリが完成します。
テスト環境構築・CI/CD デプロイ
高品質なアプリを維持するには ユニットテスト と 継続的デリバリー が不可欠です。この章では Jest と React Testing Library のセットアップ、主要コンポーネントのテスト例、そして Vercel と Netlify への自動デプロイフローを具体的に示します。
Jest と React Testing Library の設定
まずは開発依存として以下パッケージをインストールします。ts-jest は TypeScript ファイルのトランスパイルに必要です。
|
1 2 3 4 |
npm i -D jest@latest ts-jest @types/jest \ @testing-library/react @testing-library/jest-dom \ @testing-library/user-event identity-obj-proxy |
jest.config.ts(基本設定)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import type { Config } from 'jest'; const config: Config = { testEnvironment: 'jsdom', transform: { '^.+\\.(tsx|ts)?$': ['ts-jest', { tsconfig: './tsconfig.json' }], }, moduleNameMapper: { '\\.(css|less|scss)$': 'identity-obj-proxy', }, setupFilesAfterEnv: ['./jest.setup.ts'], collectCoverageFrom: ['src/**/*.{tsx,ts}', '!src/**/*.d.ts'], }; export default config; |
jest.setup.ts(マッチャー有効化)
|
1 2 |
import '@testing-library/jest-dom/extend-expect'; |
コンポーネントテストの具体例
以下は先ほど定義した TodoItem コンポーネントに対する 2 パターンのテストです。型安全 と アクセシビリティ の観点からも確認できます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// __tests__/TodoItem.test.tsx import { render, screen } from '@testing-library/react'; import TodoItem from '@/components/TodoItem'; describe('TodoItem コンポーネント', () => { test('completed が true のとき line-through クラスが付く', () => { render(<TodoItem id="1" title="テストタスク" completed />); const li = screen.getByText('テストタスク'); expect(li).toHaveClass('line-through'); }); test('completed が省略されたときはクラスが付かない', () => { render(<TodoItem id="2" title="別タスク" />); const li = screen.getByText('別タスク'); expect(li).not.toHaveClass('line-through'); }); }); |
テストは npm test(package.json の "test": "jest" エイリアス)で実行できます。CI 環境では GitHub Actions や GitLab CI に同コマンドを組み込んで、プルリクエストごとに自動検証させます。
Vercel と Netlify へのデプロイ手順
- リポジトリの接続
-
GitHub(または GitLab)へコードを push → Vercel のダッシュボードで「Import Project」→対象リポジトリを選択。Netlify でも同様に「New site from Git」を選びます。
-
ビルド設定
- ビルドコマンド:
npm run build(Vite がvite buildを内部で呼び出す) -
出力ディレクトリ:
dist(Vite のデフォルト) -
環境変数の登録
-
API エンドポイントやシークレットキーはプラットフォーム側の「Environment Variables」画面に設定。Next.js の
.env.localと同名で登録すれば自動的に注入されます。 -
プレビューと本番デプロイ
- プッシュごとにプレビュー URL が発行され、マージ時に本番環境へ自動デプロイ。Vercel の場合は
vercel.jsonでキャッシュ制御やリダイレクト設定が可能です。
Vercel 用サンプル vercel.json
|
1 2 3 4 5 6 7 8 9 10 |
{ "rewrites": [{ "source": "/api/(.*)", "destination": "/api/$1" }], "headers": [ { "source": "/(.*)", "headers": [{ "key": "Cache-Control", "value": "public, max-age=0, must-revalidate" }] } ] } |
まとめ:Jest + React Testing Library によるテスト基盤と、Vercel/Netlify の Git 連携 CI/CD を組み合わせれば、コード品質の担保とデプロイ自動化がシームレスに実現します。これで開発フロー全体が「ローカル → テスト → 本番」へと一貫したパイプラインになります。
この記事の要点
- 環境構築:公式サイトで最新 LTS の Node.js を確認し、Vite + React/TS テンプレート+Tailwind CSS v3.x で開発基盤を作る。
- React API:
useTransitionとuseIdは React 18 から利用可能。TypeScript で Props/State を厳密に型付けすればバグ抑止効果が高まる。 - Next.js App Router:
app/配下のディレクトリ構成と、Server Action は「トップレベル関数+'use server'」で実装する点を守る。 - 状態管理:
useReducerが UI の即時反応性を担い、TanStack Query v5 がサーバーデータの取得・キャッシュを統括。二者統合で楽観的 UI 更新が可能になる。 - テスト & デプロイ:Jest + React Testing Library によるユニットテストと、Git 連携の Vercel / Netlify CI/CD が実務レベルの品質保証を提供する。
上記の流れに沿ってハンズオンすれば、2026 年版 「Node.js LTS → Vite + React/TS → Tailwind CSS → Next.js App Router」 のスタックで、スケーラブルかつテストカバーされた Todo アプリを自信を持って本番にリリースできます。
参考文献
- Node.js 公式サイト – LTS リリース情報(https://nodejs.org/ja)
- Vite ドキュメント – React + TypeScript テンプレート(https://vitejs.dev/guide/#scaffolding-your-first-vite-project)
- Tailwind CSS インストールガイド(https://tailwindcss.com/docs/installation)
- React 公式ブログ – Concurrent Mode と
useTransition(2022 年リリースノート) - Next.js ドキュメント – App Router & Server Actions(https://nextjs.org/docs/app/building-your-application/routing)
- TanStack Query v5 リファレンス(https://tanstack.com/query/v5)
- Jest 公式サイト – TypeScript 設定例(https://jestjs.io/docs/getting-started)
本稿の情報は執筆時点(2026 年 7 月)に基づくものであり、バージョンや API の変更がある場合は各公式ドキュメントをご確認ください。