React独学は可能?効率的に学ぶための7つのステップ
React独学の完全ガイド。初心者が効率的にReactを習得するための7つのステップと具体的な学習方法を詳しく解説
React独学は可能?効率的に学ぶための7つのステップ
みなさん、「Reactを独学で習得できるかな?」と悩んでいませんか?
「どこから始めればいいかわからない」「プログラミングスクールに通わないと無理?」そんな風に思ったことはありませんか?
実は、React独学は十分可能です。 適切な学習プランがあれば、自分のペースで確実にスキルを身につけることができます。
この記事では、React独学の可能性と効率的な学習方法を7つのステップで詳しく解説します。 実際のコード例も交えながら、段階的な学習プランをお伝えしますので、ぜひ参考にしてください。
React独学は本当に可能?
結論から言うと、React独学は十分可能です。
独学成功の条件
React独学を成功させるためには、いくつかの条件があります。
必要な基礎知識
まず、これらの基礎知識があると学習がスムーズに進みます。
- HTML/CSS: 基本的なマークアップとスタイリング
- JavaScript: ES6以降の基本文法
- プログラミング思考: 論理的な問題解決能力
<!-- HTML/CSSの理解度チェック -->
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>React学習準備</title>
<style>
.container { max-width: 800px; margin: 0 auto; }
.button { padding: 10px 20px; background: #007bff; color: white; }
</style>
</head>
<body>
<div class="container">
<h1>React学習開始</h1>
<button class="button">スタート</button>
</div>
</body>
</html>
このHTMLコードが理解できれば、HTML/CSSの基礎は大丈夫です。
<div>
や<h1>
といったタグの意味、CSSでのスタイリング方法が分かっていることが重要です。
// JavaScriptの理解度チェック
// 基本的な関数と配列操作ができるか
const users = [
{ id: 1, name: "太郎", age: 25 },
{ id: 2, name: "花子", age: 30 }
];
// アロー関数と配列メソッド
const adultUsers = users.filter(user => user.age >= 20);
const userNames = users.map(user => user.name);
// 分割代入
const { name, age } = users[0];
// 非同期処理の基本
async function fetchData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
return data;
} catch (error) {
console.error('エラー:', error);
}
}
このJavaScriptコードを見て理解できれば、Reactの学習を始める準備ができています。
特に重要なのは、アロー関数、配列のメソッド、分割代入、非同期処理です。 これらはReactでよく使う機能なので、しっかり理解しておきましょう。
学習に必要な時間
React独学に必要な時間の目安をお伝えします。
- 平日: 1-2時間/日
- 休日: 3-5時間/日
- 期間: 3-6ヶ月(基礎から実務レベル)
無理をせず、継続できるペースで学習することが大切です。
独学のメリット・デメリット
React独学の特徴を理解しておきましょう。
メリット
独学には以下のような利点があります。
- 費用が安い: 書籍やオンライン教材のみで学習可能
- 自分のペースで学習: 時間の自由度が高い
- 実践重視: 自分で考えて問題を解決する力が身につく
デメリット
一方で、こんな課題もあります。
- 質問できる相手がいない: 困ったときのサポート不足
- 体系的でない: 学習順序を自分で決める必要
- モチベーション維持: 一人で続ける難しさ
でも大丈夫です! これらの課題は、適切な学習方法で解決できます。
これらを踏まえて、効率的な学習ステップを見ていきましょう。
ステップ1:環境構築と基本セットアップ
まず、React開発に必要な環境を整えましょう。
開発環境の準備
React開発に必要なツールをインストールします。
Node.jsとnpmの確認
# バージョン確認
node --version # v18以上推奨
npm --version # v8以上推奨
# 最新版でない場合は公式サイトからダウンロード
# https://nodejs.org/
このコマンドでバージョンを確認できます。
Node.jsはv18以上、npmはv8以上を使うことをおすすめします。 古いバージョンだと、新しいReactの機能が使えない場合があります。
エディタの準備
Visual Studio Codeに以下の拡張機能をインストールしましょう。
- ES7+ React/Redux/React-Native snippets: Reactのコード補完
- Prettier: コードフォーマッター
- ESLint: コードの品質チェック
- Auto Rename Tag: タグの自動リネーム
- Bracket Pair Colorizer: 括弧の色分け
これらの拡張機能があると、開発効率が大幅に向上します。
最初のReactプロジェクト作成
# Create React Appを使用した方法
npx create-react-app my-first-react
cd my-first-react
npm start
# Viteを使用した方法(推奨・高速)
npm create vite@latest my-react-app -- --template react
cd my-react-app
npm install
npm run dev
この2つの方法でReactプロジェクトを作成できます。
Create React Appは公式ツールで安定しています。 Viteは新しいツールですが、起動が高速でおすすめです。
どちらでも構いませんが、初心者の方はCreate React Appから始めると良いでしょう。
プロジェクト構造の理解
my-react-app/
├── public/
│ └── index.html # メインHTMLファイル
├── src/
│ ├── App.js # メインコンポーネント
│ ├── App.css # アプリのスタイル
│ ├── index.js # エントリーポイント
│ └── index.css # グローバルスタイル
├── package.json # 依存関係とスクリプト
└── README.md # プロジェクト説明
Reactプロジェクトの基本的な構造です。
src
フォルダの中に、React関連のファイルが入っています。
App.js
がメインのコンポーネントで、ここから開発を始めます。
基本的なReactコンポーネント作成
// src/App.js - 最初のコンポーネント
function App() {
return (
<div className="App">
<header className="App-header">
<h1>React学習開始!</h1>
<p>独学でReactをマスターしよう</p>
</header>
</div>
);
}
export default App;
これが最初のReactコンポーネントです。
function App()
で関数コンポーネントを定義しています。
return
の中に、HTMLのようなコード(JSX)を書きます。
className
はHTMLのclass
と同じ意味です。
Reactではclass
ではなくclassName
を使うので注意しましょう。
/* src/App.css - スタイリング */
.App {
text-align: center;
}
.App-header {
background-color: #282c34;
padding: 20px;
color: white;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
このCSSでアプリのスタイルを設定します。
Flexboxを使って、コンテンツを画面の中央に配置しています。
min-height: 100vh
で画面全体の高さを確保しています。
このステップで、Reactの開発環境が整い、最初のアプリケーションが動作することを確認できます。
ステップ2:JSXとコンポーネントの理解
Reactの核となるJSXとコンポーネントについて学習しましょう。
JSXの基本ルール
JSXはJavaScriptの拡張構文で、HTMLライクな記述ができます。
JSXの書き方
// JSXの基本的な書き方
function Welcome() {
const name = "太郎";
const isLoggedIn = true;
const hobbies = ["読書", "映画鑑賞", "プログラミング"];
return (
<div className="welcome">
{/* JSX内でのコメント */}
<h1>こんにちは、{name}さん!</h1>
{/* 条件レンダリング */}
{isLoggedIn ? (
<p>ログイン済みです</p>
) : (
<p>ログインしてください</p>
)}
{/* リストレンダリング */}
<h2>趣味一覧</h2>
<ul>
{hobbies.map((hobby, index) => (
<li key={index}>{hobby}</li>
))}
</ul>
{/* イベントハンドリング */}
<button onClick={() => alert('ボタンが押されました!')}>
クリック
</button>
</div>
);
}
このコードでJSXの基本的な書き方を理解できます。
{}
の中にJavaScriptの式を書けます。
変数の値を表示したり、計算結果を表示したりできます。
条件レンダリングでは、? :
(三項演算子)を使って条件によって表示を切り替えます。
リストレンダリングでは、map
メソッドで配列の要素を一つずつ表示します。
イベントハンドリングでは、onClick
でクリック時の処理を定義します。
JSXの重要なルール
// ❌ 間違った書き方
function BadExample() {
return (
<h1>タイトル</h1>
<p>説明文</p> // エラー:複数の要素を返せない
);
}
// ✅ 正しい書き方1:親要素で囲む
function GoodExample1() {
return (
<div>
<h1>タイトル</h1>
<p>説明文</p>
</div>
);
}
// ✅ 正しい書き方2:React.Fragmentを使用
function GoodExample2() {
return (
<>
<h1>タイトル</h1>
<p>説明文</p>
</>
);
}
// ✅ className(classではない)
function StyledComponent() {
return (
<div className="container"> {/* class ではなく className */}
<label htmlFor="email">メール</label> {/* for ではなく htmlFor */}
<input type="email" id="email" />
</div>
);
}
JSXには重要なルールがあります。
複数の要素を返すときは、必ず親要素で囲む必要があります。
親要素を作りたくない場合は、<>...</>
(React.Fragment)を使います。
また、HTMLの属性名が少し違います。
class
はclassName
、for
はhtmlFor
を使います。
これは、JavaScriptの予約語と重複するのを避けるためです。
関数コンポーネントの作成
// 基本的な関数コンポーネント
function UserCard({ user }) {
return (
<div className="user-card">
<img src={user.avatar} alt={user.name} />
<h3>{user.name}</h3>
<p>{user.email}</p>
<p>年齢: {user.age}歳</p>
</div>
);
}
// デフォルトProps
function Button({ children, type = "button", onClick }) {
return (
<button type={type} onClick={onClick} className="btn">
{children}
</button>
);
}
// 複数のコンポーネントを組み合わせ
function UserList() {
const users = [
{ id: 1, name: "太郎", email: "taro@example.com", age: 25, avatar: "/avatar1.jpg" },
{ id: 2, name: "花子", email: "hanako@example.com", age: 30, avatar: "/avatar2.jpg" },
{ id: 3, name: "次郎", email: "jiro@example.com", age: 22, avatar: "/avatar3.jpg" }
];
const handleUserClick = (user) => {
alert(`${user.name}がクリックされました`);
};
return (
<div className="user-list">
<h2>ユーザー一覧</h2>
<div className="users-grid">
{users.map(user => (
<div key={user.id} onClick={() => handleUserClick(user)}>
<UserCard user={user} />
</div>
))}
</div>
<Button onClick={() => alert('新規ユーザー追加')}>
新規ユーザー追加
</Button>
</div>
);
}
コンポーネントの作成方法を詳しく見てみましょう。
UserCardは、ユーザー情報を表示するコンポーネントです。
{user}
でprops(プロパティ)として親から受け取った値を使います。
Buttonは、再利用可能なボタンコンポーネントです。
type = "button"
でデフォルト値を設定できます。
children
は、タグの中身(子要素)を表します。
UserListは、複数のコンポーネントを組み合わせたコンポーネントです。 ユーザーのリストを表示し、クリック時の処理も定義しています。
Propsによるデータ受け渡し
// 親コンポーネント
function App() {
const blogPost = {
title: "React独学のススメ",
content: "Reactは独学でも十分習得可能です...",
author: "プログラマー太郎",
publishedDate: "2024-01-15",
tags: ["React", "独学", "プログラミング"]
};
return (
<div className="App">
<Header siteTitle="技術ブログ" />
<BlogPost post={blogPost} />
<Footer />
</div>
);
}
// 子コンポーネント
function Header({ siteTitle }) {
return (
<header className="header">
<h1>{siteTitle}</h1>
<nav>
<a href="/">ホーム</a>
<a href="/about">会社概要</a>
<a href="/contact">お問い合わせ</a>
</nav>
</header>
);
}
function BlogPost({ post }) {
return (
<article className="blog-post">
<h2>{post.title}</h2>
<div className="meta">
<span>著者: {post.author}</span>
<span>公開日: {post.publishedDate}</span>
</div>
<div className="content">
<p>{post.content}</p>
</div>
<div className="tags">
{post.tags.map(tag => (
<span key={tag} className="tag">#{tag}</span>
))}
</div>
</article>
);
}
function Footer() {
return (
<footer className="footer">
<p>© 2024 技術ブログ. All rights reserved.</p>
</footer>
);
}
Propsを使ったデータの受け渡し方法です。
親コンポーネント(App)から子コンポーネント(Header、BlogPost)にデータを渡しています。
siteTitle="技術ブログ"
のように、属性として値を渡します。
子コンポーネントでは{ siteTitle }
として受け取ります。
オブジェクトや配列も渡せます。
post={blogPost}
でオブジェクト全体を渡し、post.title
のようにプロパティにアクセスします。
この仕組みを理解すれば、コンポーネント間のデータのやり取りができるようになります。
ステップ3:State管理とHooksの習得
Reactの動的な機能を実現するStateとHooksを学習しましょう。
useState Hook
import { useState } from 'react';
// 基本的なuseStateの使用
function Counter() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
const reset = () => setCount(0);
return (
<div className="counter">
<h2>カウンター: {count}</h2>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
<button onClick={reset}>リセット</button>
<div>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="名前を入力"
/>
<p>こんにちは、{name || "名無し"}さん!</p>
</div>
</div>
);
}
useState
はReactの基本的なHookです。
const [count, setCount] = useState(0);
でstateを定義します。
count
が現在の値、setCount
が値を更新する関数です。
useState(0)
の0
が初期値になります。
ボタンをクリックするとsetCount
が呼ばれ、画面が自動的に更新されます。
これがReactのリアクティブな仕組みです。
入力フィールドでは、onChange
イベントで入力値をstateに保存しています。
// オブジェクトのStateを管理
function UserForm() {
const [user, setUser] = useState({
name: '',
email: '',
age: 0
});
const handleInputChange = (e) => {
const { name, value } = e.target;
setUser(prevUser => ({
...prevUser,
[name]: value
}));
};
const handleSubmit = (e) => {
e.preventDefault();
console.log('送信されたユーザー情報:', user);
alert(`ユーザー ${user.name} が登録されました!`);
};
return (
<form onSubmit={handleSubmit} className="user-form">
<h2>ユーザー登録</h2>
<div>
<label htmlFor="name">名前:</label>
<input
type="text"
id="name"
name="name"
value={user.name}
onChange={handleInputChange}
required
/>
</div>
<div>
<label htmlFor="email">メールアドレス:</label>
<input
type="email"
id="email"
name="email"
value={user.email}
onChange={handleInputChange}
required
/>
</div>
<div>
<label htmlFor="age">年齢:</label>
<input
type="number"
id="age"
name="age"
value={user.age}
onChange={handleInputChange}
min="0"
max="120"
/>
</div>
<button type="submit">登録</button>
<div className="preview">
<h3>プレビュー</h3>
<p>名前: {user.name}</p>
<p>メール: {user.email}</p>
<p>年齢: {user.age}歳</p>
</div>
</form>
);
}
オブジェクトをstateで管理する方法です。
useState
でオブジェクトを初期値として設定できます。
更新時は...prevUser
でスプレッド演算子を使い、既存の値をコピーしています。
[name]: value
で動的なプロパティ名を設定できます。
name
属性と同名のプロパティが更新されます。
フォームが送信されるとhandleSubmit
が実行され、ユーザー情報がコンソールに表示されます。
useEffect Hook
import { useState, useEffect } from 'react';
// 基本的なuseEffectの使用
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// マウント時とuserIdが変更された時に実行
useEffect(() => {
async function fetchUser() {
try {
setLoading(true);
setError(null);
// APIからユーザー情報を取得
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('ユーザー情報の取得に失敗しました');
}
const userData = await response.json();
setUser(userData);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
fetchUser();
}, [userId]); // 依存配列にuserIdを指定
if (loading) return <div className="loading">読み込み中...</div>;
if (error) return <div className="error">エラー: {error}</div>;
if (!user) return <div>ユーザーが見つかりません</div>;
return (
<div className="user-profile">
<img src={user.avatar} alt={user.name} />
<h2>{user.name}</h2>
<p>Email: {user.email}</p>
<p>登録日: {new Date(user.createdAt).toLocaleDateString()}</p>
</div>
);
}
useEffect
は副作用を処理するHookです。
コンポーネントがマウント(表示)されたときや、stateが変更されたときに実行されます。
useEffect
の第二引数の配列(依存配列)に指定した値が変わると、再実行されます。
この例では、userId
が変わるたびにAPIからユーザー情報を取得しています。
ローディング状態、エラー状態、成功状態を管理することで、ユーザーに適切なフィードバックを提供できます。
// クリーンアップ関数
useEffect(() => {
console.log('コンポーネントがマウントされました');
// タイマーの設定
const timer = setInterval(() => {
console.log('定期実行中...');
}, 5000);
// クリーンアップ(アンマウント時に実行)
return () => {
console.log('コンポーネントがアンマウントされます');
clearInterval(timer);
};
}, []); // 空の依存配列でマウント時のみ実行
// リアルタイムデータの更新
function LiveClock() {
const [currentTime, setCurrentTime] = useState(new Date());
useEffect(() => {
const timer = setInterval(() => {
setCurrentTime(new Date());
}, 1000);
return () => clearInterval(timer);
}, []);
return (
<div className="live-clock">
<h2>現在時刻</h2>
<p>{currentTime.toLocaleTimeString()}</p>
</div>
);
}
useEffect
ではクリーンアップ関数を返すことができます。
これは、コンポーネントがアンマウント(非表示)されるときに実行されます。 タイマーやイベントリスナーなどを解除するときに使います。
LiveClockコンポーネントでは、1秒ごとに時刻を更新しています。 コンポーネントが非表示になったとき、タイマーを自動的に停止します。
実践的なTodoアプリ
import { useState, useEffect } from 'react';
function TodoApp() {
const [todos, setTodos] = useState([]);
const [inputValue, setInputValue] = useState('');
const [filter, setFilter] = useState('all'); // all, active, completed
// ローカルストレージからデータを読み込み
useEffect(() => {
const savedTodos = localStorage.getItem('todos');
if (savedTodos) {
setTodos(JSON.parse(savedTodos));
}
}, []);
// todosが変更されたらローカルストレージに保存
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
const addTodo = () => {
if (inputValue.trim()) {
const newTodo = {
id: Date.now(),
text: inputValue.trim(),
completed: false,
createdAt: new Date().toISOString()
};
setTodos([...todos, newTodo]);
setInputValue('');
}
};
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
const clearCompleted = () => {
setTodos(todos.filter(todo => !todo.completed));
};
// フィルタリング
const filteredTodos = todos.filter(todo => {
switch (filter) {
case 'active':
return !todo.completed;
case 'completed':
return todo.completed;
default:
return true;
}
});
const completedCount = todos.filter(todo => todo.completed).length;
const activeCount = todos.length - completedCount;
return (
<div className="todo-app">
<h1>Todoアプリ</h1>
{/* 新規Todo追加 */}
<div className="todo-input">
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
placeholder="新しいタスクを入力..."
/>
<button onClick={addTodo}>追加</button>
</div>
{/* フィルター */}
<div className="filters">
<button
className={filter === 'all' ? 'active' : ''}
onClick={() => setFilter('all')}
>
すべて ({todos.length})
</button>
<button
className={filter === 'active' ? 'active' : ''}
onClick={() => setFilter('active')}
>
未完了 ({activeCount})
</button>
<button
className={filter === 'completed' ? 'active' : ''}
onClick={() => setFilter('completed')}
>
完了済み ({completedCount})
</button>
</div>
{/* Todoリスト */}
<ul className="todo-list">
{filteredTodos.map(todo => (
<li key={todo.id} className={`todo-item ${todo.completed ? 'completed' : ''}`}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span className="todo-text">{todo.text}</span>
<button onClick={() => deleteTodo(todo.id)} className="delete-btn">
削除
</button>
</li>
))}
</ul>
{/* アクション */}
<div className="actions">
{completedCount > 0 && (
<button onClick={clearCompleted}>
完了済みを削除 ({completedCount}件)
</button>
)}
</div>
</div>
);
}
実践的なTodoアプリを作成してみました。
このアプリでは、以下の機能を実装しています:
- Todo の追加・削除・完了切り替え
- フィルタリング(すべて・未完了・完了済み)
- ローカルストレージへの保存
2つのuseEffect
を使っています。
1つ目でローカルストレージからデータを読み込み、2つ目でデータを保存しています。
フィルタリングでは、filter
stateの値に応じて表示するTodoを変更しています。
switch
文を使って、条件に応じた配列を返しています。
このTodoアプリで、useStateとuseEffectの実践的な使い方を理解できます。
ステップ4:イベント処理とフォーム
ユーザーとの相互作用を実装する方法を学習しましょう。
イベントハンドリングの基本
import { useState } from 'react';
function EventExamples() {
const [message, setMessage] = useState('');
const [position, setPosition] = useState({ x: 0, y: 0 });
const [keyPressed, setKeyPressed] = useState('');
// クリックイベント
const handleClick = (e) => {
console.log('クリックされました', e.target);
setMessage(`ボタンがクリックされました: ${new Date().toLocaleTimeString()}`);
};
// マウス移動イベント
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
// キーボードイベント
const handleKeyDown = (e) => {
setKeyPressed(`押されたキー: ${e.key}`);
// 特定のキーでアクション
if (e.key === 'Enter') {
setMessage('Enterキーが押されました!');
}
};
// ダブルクリックイベント
const handleDoubleClick = () => {
setMessage('ダブルクリックされました!');
};
return (
<div className="event-examples">
<h2>イベント処理の例</h2>
<div className="section">
<h3>クリックイベント</h3>
<button onClick={handleClick}>クリック</button>
<button onDoubleClick={handleDoubleClick}>ダブルクリック</button>
<p>{message}</p>
</div>
<div className="section">
<h3>マウス位置</h3>
<div
className="mouse-area"
onMouseMove={handleMouseMove}
style={{
width: '300px',
height: '200px',
border: '1px solid #ccc',
padding: '10px'
}}
>
マウスを動かしてください
<p>X: {position.x}, Y: {position.y}</p>
</div>
</div>
<div className="section">
<h3>キーボードイベント</h3>
<input
type="text"
onKeyDown={handleKeyDown}
placeholder="何か入力してください"
/>
<p>{keyPressed}</p>
</div>
</div>
);
}
Reactでのイベント処理方法を学びましょう。
クリックイベントでは、onClick
でボタンクリック時の処理を定義します。
e.target
でクリックされた要素の情報を取得できます。
マウス移動イベントでは、onMouseMove
でマウスの座標を取得しています。
e.clientX
とe.clientY
で画面上の座標が分かります。
キーボードイベントでは、onKeyDown
で押されたキーを検知します。
e.key
で具体的なキー名を取得できます。
イベントハンドラー関数では、e
というパラメータでイベント情報を受け取ります。
複雑なフォーム処理
import { useState } from 'react';
function AdvancedForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
password: '',
confirmPassword: '',
age: '',
gender: '',
hobbies: [],
newsletter: false,
comments: ''
});
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
// 入力値の変更処理
const handleInputChange = (e) => {
const { name, value, type, checked } = e.target;
if (type === 'checkbox') {
if (name === 'hobbies') {
// 複数選択チェックボックス
setFormData(prev => ({
...prev,
hobbies: checked
? [...prev.hobbies, value]
: prev.hobbies.filter(hobby => hobby !== value)
}));
} else {
// 単一チェックボックス
setFormData(prev => ({
...prev,
[name]: checked
}));
}
} else {
setFormData(prev => ({
...prev,
[name]: value
}));
}
// エラーをクリア
if (errors[name]) {
setErrors(prev => ({
...prev,
[name]: ''
}));
}
};
// バリデーション
const validateForm = () => {
const newErrors = {};
if (!formData.name.trim()) {
newErrors.name = '名前は必須です';
}
if (!formData.email.trim()) {
newErrors.email = 'メールアドレスは必須です';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = '有効なメールアドレスを入力してください';
}
if (!formData.password) {
newErrors.password = 'パスワードは必須です';
} else if (formData.password.length < 8) {
newErrors.password = 'パスワードは8文字以上で入力してください';
}
if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = 'パスワードが一致しません';
}
if (!formData.age || formData.age < 0 || formData.age > 120) {
newErrors.age = '有効な年齢を入力してください';
}
if (!formData.gender) {
newErrors.gender = '性別を選択してください';
}
return newErrors;
};
// フォーム送信
const handleSubmit = async (e) => {
e.preventDefault();
const newErrors = validateForm();
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
setIsSubmitting(true);
try {
// API送信のシミュレーション
await new Promise(resolve => setTimeout(resolve, 2000));
console.log('送信データ:', formData);
alert('登録が完了しました!');
// フォームをリセット
setFormData({
name: '',
email: '',
password: '',
confirmPassword: '',
age: '',
gender: '',
hobbies: [],
newsletter: false,
comments: ''
});
} catch (error) {
alert('エラーが発生しました。もう一度お試しください。');
} finally {
setIsSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit} className="advanced-form">
<h2>ユーザー登録フォーム</h2>
{/* 名前 */}
<div className="form-group">
<label htmlFor="name">名前 *</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleInputChange}
className={errors.name ? 'error' : ''}
/>
{errors.name && <span className="error-message">{errors.name}</span>}
</div>
{/* メールアドレス */}
<div className="form-group">
<label htmlFor="email">メールアドレス *</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleInputChange}
className={errors.email ? 'error' : ''}
/>
{errors.email && <span className="error-message">{errors.email}</span>}
</div>
{/* 性別 */}
<div className="form-group">
<label>性別 *</label>
<div className="radio-group">
<label>
<input
type="radio"
name="gender"
value="male"
checked={formData.gender === 'male'}
onChange={handleInputChange}
/>
男性
</label>
<label>
<input
type="radio"
name="gender"
value="female"
checked={formData.gender === 'female'}
onChange={handleInputChange}
/>
女性
</label>
</div>
{errors.gender && <span className="error-message">{errors.gender}</span>}
</div>
{/* 趣味 */}
<div className="form-group">
<label>趣味</label>
<div className="checkbox-group">
{['読書', '映画鑑賞', 'スポーツ', '音楽', 'プログラミング'].map(hobby => (
<label key={hobby}>
<input
type="checkbox"
name="hobbies"
value={hobby}
checked={formData.hobbies.includes(hobby)}
onChange={handleInputChange}
/>
{hobby}
</label>
))}
</div>
</div>
<button type="submit" disabled={isSubmitting} className="submit-button">
{isSubmitting ? '送信中...' : '登録'}
</button>
</form>
);
}
複雑なフォーム処理の実装方法です。
入力値の変更処理では、入力の種類によって処理を分けています。
チェックボックスの場合はchecked
の値、その他はvalue
の値を使います。
複数選択チェックボックスでは、配列に追加・削除を行います。
checked
がtrueなら配列に追加、falseなら配列から削除します。
バリデーションでは、各項目の入力チェックを行います。 メールアドレスの形式チェックには正規表現を使用しています。
フォーム送信では、バリデーションを実行してからAPIに送信します。
送信中はisSubmitting
でボタンを無効化し、ユーザビリティを向上させています。
このフォームで、実際のWebアプリケーションで使われるフォーム処理を学べます。
ステップ5:API連携とデータ取得
実際のWebアプリケーションでは、サーバーとのデータのやり取りが必要です。
基本的なAPI連携
import { useState, useEffect } from 'react';
// カスタムフック:API呼び出し
function useApi(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchData() {
try {
setLoading(true);
setError(null);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
fetchData();
}, [url]);
return { data, loading, error };
}
カスタムフックを作成して、API呼び出しを再利用できるようにしました。
useApi
は、URLを受け取ってAPIからデータを取得します。
ローディング状態、エラー状態、データを管理して返します。
fetch
でAPIを呼び出し、response.ok
でエラーチェックを行います。
エラーが発生した場合は、適切なメッセージを設定します。
// ユーザー一覧を表示するコンポーネント
function UserListWithAPI() {
const { data: users, loading, error } = useApi('/api/users');
const [selectedUser, setSelectedUser] = useState(null);
if (loading) return <div className="loading">読み込み中...</div>;
if (error) return <div className="error">エラー: {error}</div>;
return (
<div className="user-list-api">
<h2>ユーザー一覧</h2>
<div className="users-grid">
{users.map(user => (
<div
key={user.id}
className={`user-card ${selectedUser?.id === user.id ? 'selected' : ''}`}
onClick={() => setSelectedUser(user)}
>
<img src={user.avatar} alt={user.name} />
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
))}
</div>
{selectedUser && (
<UserDetail user={selectedUser} onClose={() => setSelectedUser(null)} />
)}
</div>
);
}
// ユーザー詳細モーダル
function UserDetail({ user, onClose }) {
const [posts, setPosts] = useState([]);
const [postsLoading, setPostsLoading] = useState(true);
useEffect(() => {
async function fetchUserPosts() {
try {
setPostsLoading(true);
const response = await fetch(`/api/users/${user.id}/posts`);
const userPosts = await response.json();
setPosts(userPosts);
} catch (error) {
console.error('投稿の取得に失敗:', error);
} finally {
setPostsLoading(false);
}
}
fetchUserPosts();
}, [user.id]);
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<button className="close-button" onClick={onClose}>×</button>
<div className="user-detail">
<img src={user.avatar} alt={user.name} />
<h2>{user.name}</h2>
<p>Email: {user.email}</p>
<p>電話: {user.phone}</p>
<p>ウェブサイト: {user.website}</p>
<h3>投稿一覧</h3>
{postsLoading ? (
<p>投稿を読み込み中...</p>
) : (
<ul className="posts-list">
{posts.map(post => (
<li key={post.id}>
<h4>{post.title}</h4>
<p>{post.body.substring(0, 100)}...</p>
</li>
))}
</ul>
)}
</div>
</div>
</div>
);
}
実際のAPIからデータを取得して表示するコンポーネントです。
UserListWithAPIでは、カスタムフックuseApi
を使ってユーザー一覧を取得しています。
ユーザーカードをクリックすると、選択状態になり詳細モーダルが表示されます。
UserDetailでは、選択されたユーザーの投稿一覧を別途取得しています。
useEffect
でユーザーIDが変わるたびに投稿を取得し直します。
モーダルでは、背景クリックで閉じる機能を実装しています。
e.stopPropagation()
で、モーダル内容クリック時の閉じる動作を防いでいます。
データ操作(CRUD)
import { useState, useEffect } from 'react';
function TodoManager() {
const [todos, setTodos] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [newTodo, setNewTodo] = useState('');
const [editingId, setEditingId] = useState(null);
const [editText, setEditText] = useState('');
// データ取得(READ)
useEffect(() => {
fetchTodos();
}, []);
const fetchTodos = async () => {
try {
setLoading(true);
const response = await fetch('/api/todos');
if (!response.ok) throw new Error('取得に失敗しました');
const data = await response.json();
setTodos(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
// 新規作成(CREATE)
const createTodo = async () => {
if (!newTodo.trim()) return;
try {
const response = await fetch('/api/todos', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
text: newTodo.trim(),
completed: false
}),
});
if (!response.ok) throw new Error('作成に失敗しました');
const createdTodo = await response.json();
setTodos(prev => [...prev, createdTodo]);
setNewTodo('');
} catch (err) {
alert(`エラー: ${err.message}`);
}
};
// 更新(UPDATE)
const updateTodo = async (id, updates) => {
try {
const response = await fetch(`/api/todos/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(updates),
});
if (!response.ok) throw new Error('更新に失敗しました');
const updatedTodo = await response.json();
setTodos(prev => prev.map(todo =>
todo.id === id ? updatedTodo : todo
));
} catch (err) {
alert(`エラー: ${err.message}`);
}
};
// 削除(DELETE)
const deleteTodo = async (id) => {
if (!window.confirm('本当に削除しますか?')) return;
try {
const response = await fetch(`/api/todos/${id}`, {
method: 'DELETE',
});
if (!response.ok) throw new Error('削除に失敗しました');
setTodos(prev => prev.filter(todo => todo.id !== id));
} catch (err) {
alert(`エラー: ${err.message}`);
}
};
if (loading) return <div>読み込み中...</div>;
if (error) return <div>エラー: {error}</div>;
return (
<div className="todo-manager">
<h2>Todo管理(API連携)</h2>
{/* 新規Todo追加 */}
<div className="add-todo">
<input
type="text"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && createTodo()}
placeholder="新しいタスクを入力..."
/>
<button onClick={createTodo}>追加</button>
</div>
{/* Todoリスト */}
<ul className="todo-list">
{todos.map(todo => (
<li key={todo.id} className={`todo-item ${todo.completed ? 'completed' : ''}`}>
<input
type="checkbox"
checked={todo.completed}
onChange={(e) => updateTodo(todo.id, { completed: e.target.checked })}
/>
{editingId === todo.id ? (
<div className="edit-mode">
<input
type="text"
value={editText}
onChange={(e) => setEditText(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter') finishEdit();
if (e.key === 'Escape') cancelEdit();
}}
autoFocus
/>
<button onClick={finishEdit}>保存</button>
<button onClick={cancelEdit}>キャンセル</button>
</div>
) : (
<div className="view-mode">
<span className="todo-text">{todo.text}</span>
<div className="actions">
<button onClick={() => startEdit(todo)}>編集</button>
<button onClick={() => deleteTodo(todo.id)}>削除</button>
</div>
</div>
)}
</li>
))}
</ul>
<div className="summary">
<p>全{todos.length}件 | 完了: {todos.filter(t => t.completed).length}件</p>
</div>
</div>
);
}
CRUD操作(Create, Read, Update, Delete)を実装したTodoマネージャーです。
**CREATE(新規作成)**では、POSTメソッドでデータを送信します。
Content-Type: application/json
ヘッダーと、JSONデータを送信します。
**READ(読み取り)**では、GETメソッドでデータを取得します。 コンポーネントのマウント時に実行されます。
**UPDATE(更新)**では、PUTメソッドで既存データを更新します。 チェックボックスの状態変更や、テキストの編集で使用します。
**DELETE(削除)**では、DELETEメソッドでデータを削除します。 削除前に確認ダイアログを表示して、誤操作を防ぎます。
各操作でエラーハンドリングを行い、ユーザーに適切なフィードバックを提供しています。
この実装で、実際のWebアプリケーションで必要なAPI連携のパターンを学べます。
ステップ6:ルーティングとSPA開発
単一ページアプリケーション(SPA)の基本となるルーティングを学習しましょう。
React Router の基本
# React Routerをインストール
npm install react-router-dom
まず、React Routerをインストールします。
これは、Reactでページ遷移を実現するための標準的なライブラリです。
import { BrowserRouter as Router, Routes, Route, Link, useNavigate, useParams } from 'react-router-dom';
// メインアプリケーション
function App() {
return (
<Router>
<div className="app">
<Header />
<main>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/products" element={<ProductsPage />} />
<Route path="/products/:id" element={<ProductDetailPage />} />
<Route path="/contact" element={<ContactPage />} />
<Route path="/user/:userId/profile" element={<UserProfilePage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
</main>
<Footer />
</div>
</Router>
);
}
React Routerの基本的な設定です。
Routerで全体を囲み、Routesの中にRouteを定義します。
path
でURLパス、element
で表示するコンポーネントを指定します。
動的ルートでは、:id
のようにパラメータを指定できます。
path="*"
は、どのルートにもマッチしない場合の404ページです。
// ヘッダーコンポーネント(ナビゲーション)
function Header() {
return (
<header className="header">
<div className="container">
<h1>
<Link to="/">ECサイト</Link>
</h1>
<nav className="navigation">
<Link to="/" className="nav-link">ホーム</Link>
<Link to="/products" className="nav-link">商品一覧</Link>
<Link to="/about" className="nav-link">会社概要</Link>
<Link to="/contact" className="nav-link">お問い合わせ</Link>
</nav>
</div>
</header>
);
}
// ホームページ
function HomePage() {
const navigate = useNavigate();
const handleShopNow = () => {
navigate('/products');
};
return (
<div className="home-page">
<section className="hero">
<h2>素晴らしい商品をお届けします</h2>
<p>最新のトレンドアイテムを取り揃えています</p>
<button onClick={handleShopNow} className="cta-button">
ショッピングを始める
</button>
</section>
<section className="featured-products">
<h3>おすすめ商品</h3>
<div className="products-grid">
{[1, 2, 3].map(id => (
<div key={id} className="product-card">
<img src={`/images/product${id}.jpg`} alt={`商品${id}`} />
<h4>商品{id}</h4>
<p>¥{1000 * id}</p>
<Link to={`/products/${id}`} className="view-detail">
詳細を見る
</Link>
</div>
))}
</div>
</section>
</div>
);
}
ナビゲーションとホームページの実装です。
Linkコンポーネントでページ遷移を行います。
通常の<a>
タグではなく、Link
を使うことでSPAの仕組みを活用できます。
useNavigateは、プログラム的にページ遷移を行うHookです。 ボタンクリック時などに、JavaScriptでページを変更できます。
おすすめ商品では、/products/${id}
の形で動的なリンクを生成しています。
// 商品一覧ページ
function ProductsPage() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
const [category, setCategory] = useState('all');
useEffect(() => {
fetchProducts();
}, [category]);
const fetchProducts = async () => {
try {
setLoading(true);
const url = category === 'all'
? '/api/products'
: `/api/products?category=${category}`;
const response = await fetch(url);
const data = await response.json();
setProducts(data);
} catch (error) {
console.error('商品取得エラー:', error);
} finally {
setLoading(false);
}
};
if (loading) return <div>読み込み中...</div>;
return (
<div className="products-page">
<h2>商品一覧</h2>
<div className="filters">
<select
value={category}
onChange={(e) => setCategory(e.target.value)}
>
<option value="all">すべてのカテゴリ</option>
<option value="electronics">電子機器</option>
<option value="clothing">衣類</option>
<option value="books">書籍</option>
</select>
</div>
<div className="products-grid">
{products.map(product => (
<div key={product.id} className="product-card">
<Link to={`/products/${product.id}`}>
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p className="price">¥{product.price.toLocaleString()}</p>
<p className="description">{product.description}</p>
</Link>
</div>
))}
</div>
</div>
);
}
商品一覧ページでは、カテゴリフィルタ機能を実装しています。
useEffect
でカテゴリが変更されるたびにAPIを呼び出し、商品一覧を取得し直します。
URLにクエリパラメータを追加して、サーバー側でフィルタリングしています。
各商品カードはLink
でラップされており、クリックすると商品詳細ページに遷移します。
// 商品詳細ページ
function ProductDetailPage() {
const { id } = useParams();
const navigate = useNavigate();
const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(true);
const [quantity, setQuantity] = useState(1);
useEffect(() => {
fetchProduct();
}, [id]);
const fetchProduct = async () => {
try {
setLoading(true);
const response = await fetch(`/api/products/${id}`);
if (!response.ok) {
throw new Error('商品が見つかりません');
}
const data = await response.json();
setProduct(data);
} catch (error) {
console.error('商品詳細取得エラー:', error);
navigate('/products'); // 商品一覧に戻る
} finally {
setLoading(false);
}
};
const addToCart = () => {
console.log(`商品ID ${id} を ${quantity}個 カートに追加`);
alert(`${product.name} を ${quantity}個 カートに追加しました!`);
};
if (loading) return <div>読み込み中...</div>;
if (!product) return <div>商品が見つかりません</div>;
return (
<div className="product-detail-page">
<button onClick={() => navigate('/products')} className="back-button">
← 商品一覧に戻る
</button>
<div className="product-detail">
<div className="product-images">
<img src={product.image} alt={product.name} />
</div>
<div className="product-info">
<h1>{product.name}</h1>
<p className="price">¥{product.price.toLocaleString()}</p>
<p className="description">{product.description}</p>
<div className="purchase-section">
<div className="quantity-selector">
<label htmlFor="quantity">数量:</label>
<select
id="quantity"
value={quantity}
onChange={(e) => setQuantity(Number(e.target.value))}
>
{[...Array(10)].map((_, i) => (
<option key={i + 1} value={i + 1}>{i + 1}</option>
))}
</select>
</div>
<button onClick={addToCart} className="add-to-cart">
カートに追加
</button>
</div>
<div className="product-specs">
<h3>商品仕様</h3>
<ul>
<li>カテゴリ: {product.category}</li>
<li>ブランド: {product.brand}</li>
<li>在庫: {product.stock}個</li>
</ul>
</div>
</div>
</div>
</div>
);
}
商品詳細ページでは、useParamsを使ってURLパラメータを取得しています。
const { id } = useParams();
で、:id
の部分の値を取得できます。
このIDを使って、APIから商品詳細情報を取得します。
商品が見つからない場合は、navigate('/products')
で商品一覧ページに戻します。
エラーハンドリングとユーザビリティを両立させています。
戻るボタンもnavigate
を使って実装しており、ブラウザの戻るボタンと同じ動作をします。
より実践的なルーティング例
// ユーザープロフィールページ
function UserProfilePage() {
const { userId } = useParams();
const [user, setUser] = useState(null);
const [activeTab, setActiveTab] = useState('profile');
useEffect(() => {
fetchUser();
}, [userId]);
const fetchUser = async () => {
try {
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
setUser(userData);
} catch (error) {
console.error('ユーザー情報取得エラー:', error);
}
};
if (!user) return <div>読み込み中...</div>;
return (
<div className="user-profile-page">
<h2>{user.name}のプロフィール</h2>
<div className="tabs">
<button
className={activeTab === 'profile' ? 'active' : ''}
onClick={() => setActiveTab('profile')}
>
基本情報
</button>
<button
className={activeTab === 'orders' ? 'active' : ''}
onClick={() => setActiveTab('orders')}
>
注文履歴
</button>
<button
className={activeTab === 'reviews' ? 'active' : ''}
onClick={() => setActiveTab('reviews')}
>
レビュー
</button>
</div>
<div className="tab-content">
{activeTab === 'profile' && (
<div className="profile-info">
<p>名前: {user.name}</p>
<p>メール: {user.email}</p>
<p>登録日: {user.createdAt}</p>
</div>
)}
{activeTab === 'orders' && (
<div className="orders">
<h3>注文履歴</h3>
{/* 注文履歴の表示 */}
</div>
)}
{activeTab === 'reviews' && (
<div className="reviews">
<h3>レビュー一覧</h3>
{/* レビューの表示 */}
</div>
)}
</div>
</div>
);
}
// 404ページ
function NotFoundPage() {
const navigate = useNavigate();
return (
<div className="not-found-page">
<h2>ページが見つかりません</h2>
<p>お探しのページは存在しないか、移動された可能性があります。</p>
<button onClick={() => navigate('/')}>
ホームに戻る
</button>
</div>
);
}
ユーザープロフィールページでは、タブ切り替え機能を実装しています。
URLパラメータからユーザーIDを取得し、そのユーザーの情報を表示します。
activeTab
stateでタブの切り替えを管理しています。
404ページは、存在しないURLにアクセスした場合に表示されます。 ユーザーにエラーの説明を表示し、ホームページに戻るボタンを提供しています。
このステップで、本格的なSPAアプリケーションの基本的なルーティングを理解できます。
ステップ7:実践プロジェクトの完成
学習した内容を統合して、完全なWebアプリケーションを作成しましょう。
総合的なプロジェクト例:ブログアプリ
import { useState, useEffect, createContext, useContext } from 'react';
import { BrowserRouter as Router, Routes, Route, Link, useParams, useNavigate } from 'react-router-dom';
// 認証コンテキスト
const AuthContext = createContext();
// 認証プロバイダー
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// ローカルストレージから認証情報を復元
const savedUser = localStorage.getItem('user');
if (savedUser) {
setUser(JSON.parse(savedUser));
}
setLoading(false);
}, []);
const login = async (credentials) => {
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
});
if (!response.ok) throw new Error('ログインに失敗しました');
const userData = await response.json();
setUser(userData);
localStorage.setItem('user', JSON.stringify(userData));
return true;
} catch (error) {
console.error('ログインエラー:', error);
return false;
}
};
const logout = () => {
setUser(null);
localStorage.removeItem('user');
};
return (
<AuthContext.Provider value={{ user, login, logout, loading }}>
{children}
</AuthContext.Provider>
);
}
// 認証カスタムフック
const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
};
まず、認証システムを実装しました。
AuthContextで認証状態を管理し、アプリ全体で共有します。 AuthProviderで認証に関する機能(ログイン、ログアウト)を提供します。
ログイン情報はローカルストレージに保存して、ページを再読み込みしても状態を保持します。 useAuthカスタムフックで、コンポーネントから簡単に認証情報にアクセスできます。
// メインアプリ
function App() {
return (
<AuthProvider>
<Router>
<div className="app">
<Header />
<main className="main-content">
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/posts" element={<PostsPage />} />
<Route path="/posts/:id" element={<PostDetailPage />} />
<Route path="/write" element={<WritePostPage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/profile" element={<ProfilePage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
</main>
<Footer />
</div>
</Router>
</AuthProvider>
);
}
// ヘッダーコンポーネント
function Header() {
const { user, logout } = useAuth();
return (
<header className="header">
<div className="container">
<h1>
<Link to="/">Tech Blog</Link>
</h1>
<nav className="navigation">
<Link to="/">ホーム</Link>
<Link to="/posts">記事一覧</Link>
{user ? (
<>
<Link to="/write">記事を書く</Link>
<Link to="/profile">プロフィール</Link>
<button onClick={logout} className="logout-btn">
ログアウト
</button>
<span className="user-info">こんにちは、{user.name}さん</span>
</>
) : (
<Link to="/login">ログイン</Link>
)}
</nav>
</div>
</header>
);
}
メインアプリの構成です。
AuthProviderで全体を囲むことで、どのコンポーネントからも認証情報にアクセスできます。 Headerでは、ログイン状態によって表示するメニューを切り替えています。
ログイン中は「記事を書く」「プロフィール」「ログアウト」ボタンを表示し、未ログインの場合は「ログイン」リンクを表示します。
// ホームページ
function HomePage() {
const [featuredPosts, setFeaturedPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchFeaturedPosts();
}, []);
const fetchFeaturedPosts = async () => {
try {
const response = await fetch('/api/posts?featured=true&limit=6');
const posts = await response.json();
setFeaturedPosts(posts);
} catch (error) {
console.error('記事取得エラー:', error);
} finally {
setLoading(false);
}
};
return (
<div className="home-page">
<section className="hero">
<h2>最新の技術情報をお届け</h2>
<p>プログラミングから最新技術まで、役立つ情報を発信しています</p>
<Link to="/posts" className="cta-button">記事を読む</Link>
</section>
<section className="featured-posts">
<h3>注目の記事</h3>
{loading ? (
<div>読み込み中...</div>
) : (
<div className="posts-grid">
{featuredPosts.map(post => (
<article key={post.id} className="post-card">
<img src={post.thumbnail} alt={post.title} />
<div className="post-content">
<h4>
<Link to={`/posts/${post.id}`}>{post.title}</Link>
</h4>
<p className="excerpt">{post.excerpt}</p>
<div className="post-meta">
<span>by {post.author.name}</span>
<span>{new Date(post.createdAt).toLocaleDateString()}</span>
</div>
</div>
</article>
))}
</div>
)}
</section>
</div>
);
}
ホームページでは、注目記事を表示しています。
APIから注目記事を6件取得し、カード形式で表示します。 各記事カードには、サムネイル、タイトル、抜粋、著者名、公開日を表示しています。
ローディング状態を管理し、データ取得中は「読み込み中...」を表示します。
// 記事一覧ページ
function PostsPage() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [category, setCategory] = useState('all');
const [currentPage, setCurrentPage] = useState(1);
useEffect(() => {
fetchPosts();
}, [searchTerm, category, currentPage]);
const fetchPosts = async () => {
try {
setLoading(true);
const params = new URLSearchParams({
page: currentPage,
limit: 10,
...(searchTerm && { search: searchTerm }),
...(category !== 'all' && { category })
});
const response = await fetch(`/api/posts?${params}`);
const data = await response.json();
setPosts(data.posts);
} catch (error) {
console.error('記事取得エラー:', error);
} finally {
setLoading(false);
}
};
return (
<div className="posts-page">
<h2>記事一覧</h2>
{/* 検索・フィルター */}
<div className="filters">
<input
type="text"
placeholder="記事を検索..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="search-input"
/>
<select
value={category}
onChange={(e) => setCategory(e.target.value)}
className="category-select"
>
<option value="all">すべてのカテゴリ</option>
<option value="javascript">JavaScript</option>
<option value="react">React</option>
<option value="nodejs">Node.js</option>
<option value="css">CSS</option>
</select>
</div>
{/* 記事リスト */}
{loading ? (
<div>読み込み中...</div>
) : (
<>
<div className="posts-list">
{posts.map(post => (
<article key={post.id} className="post-item">
<div className="post-thumbnail">
<img src={post.thumbnail} alt={post.title} />
</div>
<div className="post-content">
<h3>
<Link to={`/posts/${post.id}`}>{post.title}</Link>
</h3>
<p className="excerpt">{post.excerpt}</p>
<div className="post-meta">
<span className="author">by {post.author.name}</span>
<span className="date">
{new Date(post.createdAt).toLocaleDateString()}
</span>
<span className="category">{post.category}</span>
</div>
<div className="post-stats">
<span>👀 {post.views}</span>
<span>❤️ {post.likes}</span>
<span>💬 {post.comments.length}</span>
</div>
</div>
</article>
))}
</div>
{/* ページネーション */}
<div className="pagination">
<button
disabled={currentPage === 1}
onClick={() => setCurrentPage(prev => prev - 1)}
>
前のページ
</button>
<span>ページ {currentPage}</span>
<button
onClick={() => setCurrentPage(prev => prev + 1)}
>
次のページ
</button>
</div>
</>
)}
</div>
);
}
記事一覧ページでは、検索・フィルタ・ページネーション機能を実装しています。
検索キーワード、カテゴリ、ページ番号が変更されるたびに、APIから記事一覧を取得し直します。 URLSearchParamsを使って、クエリパラメータを構築しています。
各記事には、閲覧数、いいね数、コメント数などの統計情報も表示しています。
ページネーションでは、前のページ・次のページボタンで記事を切り替えできます。
さらに高度な機能の実装
// 記事詳細ページ
function PostDetailPage() {
const { id } = useParams();
const { user } = useAuth();
const [post, setPost] = useState(null);
const [comments, setComments] = useState([]);
const [newComment, setNewComment] = useState('');
const [loading, setLoading] = useState(true);
const [liked, setLiked] = useState(false);
useEffect(() => {
fetchPost();
fetchComments();
}, [id]);
const handleLike = async () => {
if (!user) {
alert('ログインが必要です');
return;
}
try {
const response = await fetch(`/api/posts/${id}/like`, {
method: liked ? 'DELETE' : 'POST',
headers: {
'Authorization': `Bearer ${user.token}`
}
});
if (response.ok) {
setLiked(!liked);
setPost(prev => ({
...prev,
likes: liked ? prev.likes - 1 : prev.likes + 1
}));
}
} catch (error) {
console.error('いいねエラー:', error);
}
};
const submitComment = async (e) => {
e.preventDefault();
if (!user) {
alert('ログインが必要です');
return;
}
if (!newComment.trim()) return;
try {
const response = await fetch(`/api/posts/${id}/comments`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${user.token}`
},
body: JSON.stringify({ content: newComment })
});
if (response.ok) {
const comment = await response.json();
setComments(prev => [...prev, comment]);
setNewComment('');
}
} catch (error) {
console.error('コメント投稿エラー:', error);
}
};
if (loading || !post) return <div>読み込み中...</div>;
return (
<div className="post-detail-page">
<article className="post-detail">
<header className="post-header">
<img src={post.thumbnail} alt={post.title} className="post-image" />
<h1>{post.title}</h1>
<div className="post-meta">
<div className="author-info">
<img src={post.author.avatar} alt={post.author.name} />
<div>
<span className="author-name">{post.author.name}</span>
<span className="post-date">
{new Date(post.createdAt).toLocaleDateString()}
</span>
</div>
</div>
<div className="post-actions">
<button
onClick={handleLike}
className={`like-button ${liked ? 'liked' : ''}`}
>
❤️ {post.likes}
</button>
<span>👀 {post.views}</span>
</div>
</div>
</header>
<div className="post-content">
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</div>
<div className="post-tags">
{post.tags.map(tag => (
<span key={tag} className="tag">#{tag}</span>
))}
</div>
</article>
{/* コメントセクション */}
<section className="comments-section">
<h3>コメント ({comments.length})</h3>
{user && (
<form onSubmit={submitComment} className="comment-form">
<textarea
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
placeholder="コメントを入力..."
rows="4"
/>
<button type="submit">コメント投稿</button>
</form>
)}
<div className="comments-list">
{comments.map(comment => (
<div key={comment.id} className="comment">
<div className="comment-header">
<img src={comment.author.avatar} alt={comment.author.name} />
<div>
<span className="comment-author">{comment.author.name}</span>
<span className="comment-date">
{new Date(comment.createdAt).toLocaleDateString()}
</span>
</div>
</div>
<p className="comment-content">{comment.content}</p>
</div>
))}
</div>
</section>
</div>
);
}
記事詳細ページでは、いいね機能とコメント機能を実装しています。
いいね機能では、ログイン状態をチェックし、APIにリクエストを送信します。 いいね状態に応じて、POSTまたはDELETEメソッドを使い分けています。
コメント機能では、ログインユーザーのみがコメントを投稿できます。 認証トークンをヘッダーに含めて、APIに送信します。
dangerouslySetInnerHTML
でHTMLコンテンツを表示していますが、実際のアプリケーションではXSS対策が必要です。
このブログアプリの実装で、本格的なWebアプリケーション開発のスキルを習得できます。
学習の継続とコミュニティ活用
React独学を成功させるためのコツをお伝えします。
学習の継続方法
日々の学習習慣
継続的な学習のために、以下の習慣を身につけましょう。
- 毎日のコミット: GitHubに学習記録を残す
- 進捗の可視化: 学習時間や完了項目を記録
- 小さな目標設定: 週単位での達成可能な目標
// 学習進捗を管理するコンポーネント例
function LearningTracker() {
const [progress, setProgress] = useState({
totalHours: 0,
completedTasks: [],
currentGoal: '',
achievements: []
});
const addStudyTime = (hours) => {
setProgress(prev => ({
...prev,
totalHours: prev.totalHours + hours
}));
};
const completeTask = (task) => {
setProgress(prev => ({
...prev,
completedTasks: [...prev.completedTasks, {
...task,
completedAt: new Date().toISOString()
}]
}));
};
return (
<div className="learning-tracker">
<h3>学習進捗</h3>
<p>総学習時間: {progress.totalHours}時間</p>
<p>完了タスク: {progress.completedTasks.length}個</p>
<div className="recent-achievements">
<h4>最近の成果</h4>
{progress.completedTasks.slice(-5).map(task => (
<div key={task.id}>
{task.name} - {new Date(task.completedAt).toLocaleDateString()}
</div>
))}
</div>
</div>
);
}
学習進捗を可視化するコンポーネントの例です。
学習時間や完了したタスクを記録することで、モチベーションの維持につながります。 小さな成果でも記録することで、着実な成長を実感できます。
コミュニティ活用
オンラインコミュニティ
React学習を助けてくれるコミュニティを活用しましょう。
- React Tokyo: 日本最大級のReactコミュニティ
- Discord/Slack: リアルタイムでの質問・相談
- Twitter: 技術情報の収集と発信
- Qiita: 学習記録の公開
学習リソース
効果的な学習リソースを活用しましょう。
- 公式ドキュメント: 最新で正確な情報
- React DevTools: デバッグツール
- CodeSandbox: オンラインエディタ
- GitHub: オープンソースコードの学習
質問する前に、公式ドキュメントやGoogle検索で調べる習慣をつけましょう。 自分で調べる力も、プログラマーにとって重要なスキルです。
質問するときは、具体的なエラーメッセージや試したことを明記すると、適切な回答を得やすくなります。
まとめ
React独学について7つのステップで詳しく解説しました。
React独学は可能
適切な学習プランと継続的な実践により、独学でもReactを習得できます。
段階的な学習が重要
基礎から応用まで、7つのステップで着実にスキルアップできます。
実践プロジェクトが効果的
理論だけでなく、実際にアプリケーションを作ることで理解が深まります。
コミュニティの活用
一人で学習するデメリットは、コミュニティ参加で補えます。
継続学習が鍵
Reactは進化が早いため、継続的な学習が必要です。
React独学は決して簡単ではありませんが、この7つのステップを参考に学習を進めることで、確実にスキルを身につけることができます。
自分のペースで学習を続け、実践的なプロジェクトを通じてReact開発の楽しさを実感してください。
きっと、モダンなフロントエンド開発者として活躍できるはずです!