JavaScriptからReactへ - スムーズに移行するための学習ロードマップ

JavaScript経験者がReactへスムーズに移行するための完全ロードマップ。段階的学習法、実践プロジェクト、つまずきポイントの対策まで詳しく解説します。

Learning Next 運営
59 分で読めます

JavaScriptは使えるけど、Reactって何だか難しそうですよね?

「JavaScriptとReactって何が違うの?」って思いませんか? 「いきなりReactを始めても大丈夫?」って不安になりますよね。 そんな気持ち、すごくよくわかります。

この記事では、JavaScript経験者がReactへスムーズに移行できる完全ロードマップをお伝えします。

段階的な学習法と実践的なプロジェクトで、挫折せずにReactマスターになれます。 読み終わる頃には、自信を持ってReact開発に取り組めるはずです。

JavaScriptからReactへの移行ロードマップ全体像

学習期間の目安

JavaScript経験者がReactを習得するまでの期間をお伝えします。

JavaScript基礎レベル

  • React基本概念の理解: 2-3週間
  • 実用的なアプリ作成: 1-2ヶ月
  • 中級レベル到達: 3-4ヶ月

JavaScript中級レベル

  • React基本概念の理解: 1-2週間
  • 実用的なアプリ作成: 3-4週間
  • 中級レベル到達: 2-3ヶ月

移行ロードマップの構成

学習の流れはこんな感じです。

Phase 1: JavaScript知識の整理・補強 (1-2週間)
Phase 2: React基本概念の習得 (2-3週間)
Phase 3: 実践プロジェクト開発 (4-6週間)
Phase 4: 応用技術の習得 (4-8週間)
Phase 5: 実務レベルのスキル獲得 (継続的)

それぞれのフェーズを詳しく見ていきましょう。

Phase 1: JavaScript知識の整理・補強(1-2週間)

React学習に必要なJavaScript知識

1.1 ES6+ 文法の確認

Reactでは最新のJavaScript文法を頻繁に使用します。 まずは基本的な文法を確認しましょう。

// アロー関数
const greet = (name) => `Hello, ${name}!`;

// テンプレートリテラル
const message = `こんにちは、${name}さん!
今日は${new Date().toLocaleDateString()}です。`;

// 分割代入
const user = { name: '田中', age: 25, city: '東京' };
const { name, age } = user;

const colors = ['red', 'green', 'blue'];
const [primary, secondary] = colors;

// スプレッド演算子
const newUser = { ...user, age: 26 };
const allColors = [...colors, 'yellow'];

// デフォルトパラメータ
function createUser(name, age = 25, active = true) {
  return { name, age, active };
}

アロー関数とテンプレートリテラルが使えると、Reactがぐっと書きやすくなります。

分割代入とスプレッド演算子は、Reactでは必須の技術です。 特にスプレッド演算子は、状態を更新する時によく使います。

1.2 配列メソッドの習得

Reactでは配列操作が非常に重要です。 特にmapfilterreduceは頻繁に使います。

const users = [
  { id: 1, name: '田中', age: 25, active: true },
  { id: 2, name: '佐藤', age: 30, active: false },
  { id: 3, name: '鈴木', age: 22, active: true }
];

// map: 配列の変換(ReactのJSXで頻用)
const userNames = users.map(user => user.name);
const userElements = users.map(user => `<div>${user.name}</div>`);

// filter: 条件に合う要素の抽出
const activeUsers = users.filter(user => user.active);
const adults = users.filter(user => user.age >= 25);

// find: 条件に合う最初の要素
const targetUser = users.find(user => user.id === 2);

// reduce: 配列の集約
const totalAge = users.reduce((sum, user) => sum + user.age, 0);

// some/every: 条件チェック
const hasActiveUser = users.some(user => user.active);
const allAdults = users.every(user => user.age >= 18);

mapメソッドは、ReactでJSXを生成する時に必ず使います。 配列の要素を一つずつ処理して、新しい配列を作る感じです。

filterメソッドは、条件に合う要素だけを抽出する時に使います。 例えば、アクティブなユーザーだけを表示したい時などに便利です。

1.3 オブジェクトとクラス

オブジェクトの操作も重要です。

// オブジェクトのプロパティ操作
const userData = {
  name: '田中',
  profile: {
    age: 25,
    city: '東京'
  }
};

// プロパティの動的アクセス
const key = 'name';
console.log(userData[key]); // '田中'

// オブジェクトの複製(浅いコピー)
const updatedUser = { ...userData, name: '佐藤' };

// ネストしたオブジェクトの更新
const updatedProfile = {
  ...userData,
  profile: {
    ...userData.profile,
    age: 26
  }
};

// クラスの基本(React クラスコンポーネント理解用)
class User {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  greet() {
    return `こんにちは、${this.name}です`;
  }
  
  static createGuest() {
    return new User('ゲスト', 0);
  }
}

オブジェクトの複製は、Reactの状態管理で頻繁に使います。 スプレッド演算子を使って、元のオブジェクトを変更せずに新しいオブジェクトを作る感じです。

1.4 非同期処理

API通信には必須の知識です。

// Promise の基本
function fetchUserData(userId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (userId > 0) {
        resolve({ id: userId, name: 'ユーザー' + userId });
      } else {
        reject(new Error('無効なユーザーID'));
      }
    }, 1000);
  });
}

// async/await
async function loadUserProfile(userId) {
  try {
    const user = await fetchUserData(userId);
    const profile = await fetchUserProfile(user.id);
    return { user, profile };
  } catch (error) {
    console.error('データの取得に失敗:', error);
    return null;
  }
}

// Fetch API
async function fetchUsers() {
  try {
    const response = await fetch('/api/users');
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    const users = await response.json();
    return users;
  } catch (error) {
    console.error('ユーザーデータの取得エラー:', error);
    throw error;
  }
}

async/awaitは、Promise をより読みやすく書くための記法です。 非同期処理が同期処理のように書けるので、とても便利です。

Fetch APIは、サーバーとの通信に使います。 エラーハンドリングも忘れずに行いましょう。

1.5 モジュールシステム

コードを整理するのに重要な仕組みです。

// utils.js - ユーティリティ関数のエクスポート
export function formatDate(date) {
  return date.toLocaleDateString('ja-JP');
}

export function capitalizeFirstLetter(string) {
  return string.charAt(0).toUpperCase() + string.slice(1);
}

// デフォルトエクスポート
export default class ApiClient {
  constructor(baseURL) {
    this.baseURL = baseURL;
  }
  
  async get(endpoint) {
    const response = await fetch(`${this.baseURL}${endpoint}`);
    return response.json();
  }
}

// main.js - インポート
import ApiClient, { formatDate, capitalizeFirstLetter } from './utils.js';

// 名前を変更してインポート
import { formatDate as formatJapaneseDate } from './utils.js';

// すべてをインポート
import * as utils from './utils.js';

exportimportを使って、コードを複数のファイルに分割できます。 Reactでは必ず使う機能なので、しっかり理解しておきましょう。

JavaScript復習のチェックリスト

以下が理解できていればReact学習準備完了です。

  • アロー関数とthisの違い
  • 分割代入とスプレッド演算子
  • 配列メソッド(map、filter、reduce)
  • async/awaitとPromise
  • import/export文
  • オブジェクトの操作とイミューダブルな更新

これらができれば、React学習の準備は完璧です。

Phase 2: React基本概念の習得(2-3週間)

2.1 Reactの基本概念理解

JSXの理解

JSXはJavaScriptの中にHTML風の記法を書ける仕組みです。 従来の方法と比べると、とても楽になります。

// JavaScriptでのDOM操作(従来の方法)
function createUserCard(user) {
  const div = document.createElement('div');
  div.className = 'user-card';
  
  const h3 = document.createElement('h3');
  h3.textContent = user.name;
  
  const p = document.createElement('p');
  p.textContent = `年齢: ${user.age}歳`;
  
  div.appendChild(h3);
  div.appendChild(p);
  
  return div;
}

// ReactのJSX(新しい方法)
function UserCard({ user }) {
  return (
    <div className="user-card">
      <h3>{user.name}</h3>
      <p>年齢: {user.age}歳</p>
    </div>
  );
}

従来の方法だと、createElementを何回も書く必要がありました。 JSXなら、HTMLを書くような感覚でコンポーネントが作れます。

コンポーネントの概念

コンポーネントは、UIの部品を作る仕組みです。

// 関数コンポーネント(推奨)
function Welcome({ name }) {
  return <h1>こんにちは、{name}さん!</h1>;
}

// 使用例
function App() {
  return (
    <div>
      <Welcome name="田中" />
      <Welcome name="佐藤" />
      <Welcome name="鈴木" />
    </div>
  );
}

// クラスコンポーネント(理解用)
class WelcomeClass extends React.Component {
  render() {
    return <h1>こんにちは、{this.props.name}さん!</h1>;
  }
}

関数コンポーネントが主流です。 関数を書くような感覚で、UIの部品が作れます。

2.2 State(状態管理)の理解

JavaScriptでの状態管理(従来)

従来の方法では、状態の管理が大変でした。

// JavaScriptでのカウンター
let count = 0;

function updateCounter() {
  count++;
  document.getElementById('counter').textContent = count;
}

function resetCounter() {
  count = 0;
  document.getElementById('counter').textContent = count;
}

// HTML
// <div id="counter">0</div>
// <button onclick="updateCounter()">+1</button>
// <button onclick="resetCounter()">リセット</button>

状態が変わるたびに、手動でDOMを更新する必要がありました。 面倒ですし、バグも起きやすいです。

ReactでのState管理

Reactなら、状態管理がとても簡単です。

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  
  const increment = () => {
    setCount(count + 1);
  };
  
  const reset = () => {
    setCount(0);
  };
  
  return (
    <div>
      <div>{count}</div>
      <button onClick={increment}>+1</button>
      <button onClick={reset}>リセット</button>
    </div>
  );
}

useStateを使うと、状態の管理がとても楽になります。 状態が変わると、自動的にUIも更新されます。

複数のStateの管理

複数の状態を管理する場合の例です。

function UserProfile() {
  const [user, setUser] = useState({
    name: '',
    email: '',
    age: 0
  });
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);
  
  const updateName = (newName) => {
    setUser({ ...user, name: newName });
    // または
    setUser(prevUser => ({ ...prevUser, name: newName }));
  };
  
  const loadUser = async (userId) => {
    setIsLoading(true);
    setError(null);
    
    try {
      const userData = await fetchUser(userId);
      setUser(userData);
    } catch (err) {
      setError(err.message);
    } finally {
      setIsLoading(false);
    }
  };
  
  return (
    <div>
      {isLoading && <p>読み込み中...</p>}
      {error && <p>エラー: {error}</p>}
      <input 
        value={user.name}
        onChange={(e) => updateName(e.target.value)}
        placeholder="名前"
      />
      <p>ユーザー名: {user.name}</p>
    </div>
  );
}

状態ごとにuseStateを使い分けます。 オブジェクトの状態を更新する時は、スプレッド演算子を使って新しいオブジェクトを作ります。

2.3 Props(プロパティ)の理解

Propsは、コンポーネント間でデータを渡すための仕組みです。

// 子コンポーネント
function ProductCard({ product, onAddToCart }) {
  return (
    <div className="product-card">
      <img src={product.image} alt={product.name} />
      <h3>{product.name}</h3>
      <p>価格: ¥{product.price.toLocaleString()}</p>
      <button onClick={() => onAddToCart(product)}>
        カートに追加
      </button>
    </div>
  );
}

// 親コンポーネント
function ProductList() {
  const [cart, setCart] = useState([]);
  
  const products = [
    { id: 1, name: 'ノートPC', price: 98000, image: 'laptop.jpg' },
    { id: 2, name: 'マウス', price: 3000, image: 'mouse.jpg' }
  ];
  
  const handleAddToCart = (product) => {
    setCart([...cart, product]);
  };
  
  return (
    <div>
      <h2>商品一覧</h2>
      <div className="product-grid">
        {products.map(product => (
          <ProductCard 
            key={product.id}
            product={product}
            onAddToCart={handleAddToCart}
          />
        ))}
      </div>
      <p>カート内商品数: {cart.length}</p>
    </div>
  );
}

親コンポーネントから子コンポーネントへ、データや関数を渡します。 子コンポーネントは、受け取ったPropsを使って画面を表示します。

2.4 イベントハンドリング

ユーザーの操作に反応するための仕組みです。

function ContactForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: ''
  });
  
  // 入力フィールドの変更
  const handleInputChange = (e) => {
    const { name, value } = e.target;
    setFormData({
      ...formData,
      [name]: value
    });
  };
  
  // フォーム送信
  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('送信データ:', formData);
    // API送信処理など
  };
  
  // キー入力イベント
  const handleKeyPress = (e) => {
    if (e.key === 'Enter' && e.ctrlKey) {
      handleSubmit(e);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        name="name"
        value={formData.name}
        onChange={handleInputChange}
        placeholder="お名前"
      />
      <input
        type="email"
        name="email"
        value={formData.email}
        onChange={handleInputChange}
        placeholder="メールアドレス"
      />
      <textarea
        name="message"
        value={formData.message}
        onChange={handleInputChange}
        onKeyPress={handleKeyPress}
        placeholder="メッセージ"
      />
      <button type="submit">送信</button>
    </form>
  );
}

イベントハンドラーは、ユーザーの操作に反応する関数です。 onChangeonClickなどのプロパティで指定します。

Phase 3: 実践プロジェクト開発(4-6週間)

3.1 ToDoアプリの作成

JavaScriptからReactへの移行を実感できる定番プロジェクトです。 まずは従来の方法と比較してみましょう。

JavaScript版(比較用)

// JavaScript版 ToDoアプリ
class TodoApp {
  constructor() {
    this.todos = [];
    this.nextId = 1;
    this.init();
  }
  
  init() {
    this.render();
    this.setupEventListeners();
  }
  
  addTodo(text) {
    const todo = {
      id: this.nextId++,
      text: text,
      completed: false
    };
    this.todos.push(todo);
    this.render();
  }
  
  toggleTodo(id) {
    const todo = this.todos.find(t => t.id === id);
    if (todo) {
      todo.completed = !todo.completed;
      this.render();
    }
  }
  
  deleteTodo(id) {
    this.todos = this.todos.filter(t => t.id !== id);
    this.render();
  }
  
  render() {
    const container = document.getElementById('todo-container');
    container.innerHTML = `
      <div>
        <input type="text" id="todo-input" placeholder="新しいタスク">
        <button id="add-btn">追加</button>
      </div>
      <ul>
        ${this.todos.map(todo => `
          <li class="${todo.completed ? 'completed' : ''}">
            <input type="checkbox" ${todo.completed ? 'checked' : ''} 
                   onchange="todoApp.toggleTodo(${todo.id})">
            <span>${todo.text}</span>
            <button onclick="todoApp.deleteTodo(${todo.id})">削除</button>
          </li>
        `).join('')}
      </ul>
    `;
  }
  
  setupEventListeners() {
    document.addEventListener('click', (e) => {
      if (e.target.id === 'add-btn') {
        const input = document.getElementById('todo-input');
        if (input.value.trim()) {
          this.addTodo(input.value.trim());
          input.value = '';
        }
      }
    });
  }
}

const todoApp = new TodoApp();

従来の方法では、DOM操作とイベント処理を手動で行う必要がありました。 コードが複雑になりがちで、メンテナンスも大変です。

React版

import { useState } from 'react';

function TodoApp() {
  const [todos, setTodos] = useState([]);
  const [inputValue, setInputValue] = useState('');
  const [nextId, setNextId] = useState(1);
  
  const addTodo = () => {
    if (inputValue.trim()) {
      const newTodo = {
        id: nextId,
        text: inputValue.trim(),
        completed: false
      };
      setTodos([...todos, newTodo]);
      setInputValue('');
      setNextId(nextId + 1);
    }
  };
  
  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 handleKeyPress = (e) => {
    if (e.key === 'Enter') {
      addTodo();
    }
  };
  
  return (
    <div>
      <div>
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          onKeyPress={handleKeyPress}
          placeholder="新しいタスク"
        />
        <button onClick={addTodo}>追加</button>
      </div>
      
      <ul>
        {todos.map(todo => (
          <TodoItem
            key={todo.id}
            todo={todo}
            onToggle={toggleTodo}
            onDelete={deleteTodo}
          />
        ))}
      </ul>
      
      <div>
        <p>全タスク: {todos.length}</p>
        <p>完了: {todos.filter(t => t.completed).length}</p>
        <p>未完了: {todos.filter(t => !t.completed).length}</p>
      </div>
    </div>
  );
}

function TodoItem({ todo, onToggle, onDelete }) {
  return (
    <li className={todo.completed ? 'completed' : ''}>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      <span>{todo.text}</span>
      <button onClick={() => onDelete(todo.id)}>削除</button>
    </li>
  );
}

export default TodoApp;

React版は、状態管理が簡単で、コードも読みやすいです。 コンポーネントに分割することで、再利用性も高まります。

3.2 天気アプリの作成

API連携を学べるプロジェクトです。

import { useState, useEffect } from 'react';

function WeatherApp() {
  const [weather, setWeather] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [location, setLocation] = useState('Tokyo');
  
  const fetchWeather = async (city) => {
    setLoading(true);
    setError(null);
    
    try {
      const response = await fetch(
        `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=YOUR_API_KEY&units=metric&lang=ja`
      );
      
      if (!response.ok) {
        throw new Error('天気データの取得に失敗しました');
      }
      
      const data = await response.json();
      setWeather(data);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };
  
  useEffect(() => {
    fetchWeather(location);
  }, []);
  
  const handleLocationSubmit = (e) => {
    e.preventDefault();
    const formData = new FormData(e.target);
    const newLocation = formData.get('location');
    if (newLocation) {
      setLocation(newLocation);
      fetchWeather(newLocation);
    }
  };
  
  return (
    <div className="weather-app">
      <h1>天気アプリ</h1>
      
      <form onSubmit={handleLocationSubmit}>
        <input
          type="text"
          name="location"
          placeholder="都市名を入力"
          defaultValue={location}
        />
        <button type="submit">検索</button>
      </form>
      
      {loading && <LoadingSpinner />}
      {error && <ErrorMessage message={error} />}
      {weather && <WeatherDisplay weather={weather} />}
    </div>
  );
}

function LoadingSpinner() {
  return <div className="loading">読み込み中...</div>;
}

function ErrorMessage({ message }) {
  return <div className="error">エラー: {message}</div>;
}

function WeatherDisplay({ weather }) {
  return (
    <div className="weather-display">
      <h2>{weather.name}</h2>
      <div className="current-weather">
        <img
          src={`https://openweathermap.org/img/wn/${weather.weather[0].icon}@2x.png`}
          alt={weather.weather[0].description}
        />
        <div>
          <p className="temperature">{Math.round(weather.main.temp)}°C</p>
          <p className="description">{weather.weather[0].description}</p>
        </div>
      </div>
      <div className="weather-details">
        <p>体感温度: {Math.round(weather.main.feels_like)}°C</p>
        <p>湿度: {weather.main.humidity}%</p>
        <p>風速: {weather.wind.speed} m/s</p>
      </div>
    </div>
  );
}

export default WeatherApp;

天気アプリでは、useEffectを使ってAPI呼び出しを行います。 ローディング状態とエラー状態の管理も重要です。

3.3 ショッピングカートアプリ

より複雑な状態管理を学べるプロジェクトです。

import { useState } from 'react';

function ShoppingApp() {
  const [products] = useState([
    { id: 1, name: 'ノートPC', price: 98000, image: 'laptop.jpg' },
    { id: 2, name: 'マウス', price: 3000, image: 'mouse.jpg' },
    { id: 3, name: 'キーボード', price: 8000, image: 'keyboard.jpg' }
  ]);
  
  const [cart, setCart] = useState([]);
  
  const addToCart = (product) => {
    const existingItem = cart.find(item => item.id === product.id);
    
    if (existingItem) {
      setCart(cart.map(item =>
        item.id === product.id
          ? { ...item, quantity: item.quantity + 1 }
          : item
      ));
    } else {
      setCart([...cart, { ...product, quantity: 1 }]);
    }
  };
  
  const removeFromCart = (productId) => {
    setCart(cart.filter(item => item.id !== productId));
  };
  
  const updateQuantity = (productId, newQuantity) => {
    if (newQuantity <= 0) {
      removeFromCart(productId);
      return;
    }
    
    setCart(cart.map(item =>
      item.id === productId
        ? { ...item, quantity: newQuantity }
        : item
    ));
  };
  
  const getTotalPrice = () => {
    return cart.reduce((total, item) => total + item.price * item.quantity, 0);
  };
  
  const getTotalItems = () => {
    return cart.reduce((total, item) => total + item.quantity, 0);
  };
  
  return (
    <div className="shopping-app">
      <header>
        <h1>オンラインショップ</h1>
        <div className="cart-summary">
          カート: {getTotalItems()}個 (¥{getTotalPrice().toLocaleString()})
        </div>
      </header>
      
      <main>
        <section className="products">
          <h2>商品一覧</h2>
          <div className="product-grid">
            {products.map(product => (
              <ProductCard
                key={product.id}
                product={product}
                onAddToCart={addToCart}
              />
            ))}
          </div>
        </section>
        
        <section className="cart">
          <h2>ショッピングカート</h2>
          <Cart
            items={cart}
            onUpdateQuantity={updateQuantity}
            onRemoveItem={removeFromCart}
            totalPrice={getTotalPrice()}
          />
        </section>
      </main>
    </div>
  );
}

function ProductCard({ product, onAddToCart }) {
  return (
    <div className="product-card">
      <img src={product.image} alt={product.name} />
      <h3>{product.name}</h3>
      <p className="price">¥{product.price.toLocaleString()}</p>
      <button onClick={() => onAddToCart(product)}>
        カートに追加
      </button>
    </div>
  );
}

function Cart({ items, onUpdateQuantity, onRemoveItem, totalPrice }) {
  if (items.length === 0) {
    return <p>カートは空です</p>;
  }
  
  return (
    <div className="cart">
      {items.map(item => (
        <CartItem
          key={item.id}
          item={item}
          onUpdateQuantity={onUpdateQuantity}
          onRemove={onRemoveItem}
        />
      ))}
      <div className="cart-total">
        <strong>合計: ¥{totalPrice.toLocaleString()}</strong>
      </div>
      <button className="checkout-btn">購入手続きへ</button>
    </div>
  );
}

function CartItem({ item, onUpdateQuantity, onRemove }) {
  return (
    <div className="cart-item">
      <img src={item.image} alt={item.name} />
      <div className="item-details">
        <h4>{item.name}</h4>
        <p>¥{item.price.toLocaleString()}</p>
      </div>
      <div className="quantity-controls">
        <button onClick={() => onUpdateQuantity(item.id, item.quantity - 1)}>
          -
        </button>
        <span>{item.quantity}</span>
        <button onClick={() => onUpdateQuantity(item.id, item.quantity + 1)}>
          +
        </button>
      </div>
      <div className="item-total">
        ¥{(item.price * item.quantity).toLocaleString()}
      </div>
      <button onClick={() => onRemove(item.id)}>削除</button>
    </div>
  );
}

export default ShoppingApp;

ショッピングカートアプリでは、商品の追加、削除、数量変更などの複雑な状態管理が必要です。 コンポーネントを適切に分割することで、コードの見通しが良くなります。

Phase 4: 応用技術の習得(4-8週間)

4.1 useEffectによる副作用の管理

useEffectは、コンポーネントの外部とのやり取りを管理するフックです。

import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  // コンポーネントマウント時とuserIdが変更された時に実行
  useEffect(() => {
    async function fetchUser() {
      setLoading(true);
      setError(null);
      
      try {
        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);
      }
    }
    
    if (userId) {
      fetchUser();
    }
  }, [userId]); // 依存配列にuserIdを指定
  
  // クリーンアップ関数(コンポーネントのアンマウント時)
  useEffect(() => {
    const interval = setInterval(() => {
      console.log('定期実行中...');
    }, 5000);
    
    return () => {
      clearInterval(interval); // クリーンアップ
    };
  }, []);
  
  if (loading) return <div>読み込み中...</div>;
  if (error) return <div>エラー: {error}</div>;
  if (!user) return <div>ユーザーが見つかりません</div>;
  
  return (
    <div>
      <h2>{user.name}</h2>
      <p>メール: {user.email}</p>
    </div>
  );
}

useEffectの第二引数は依存配列と呼ばれます。 配列内の値が変更された時にのみ、エフェクトが実行されます。

クリーンアップ関数を返すことで、メモリリークを防げます。

4.2 カスタムフック

共通の処理を再利用するためのフックです。

// カスタムフック:API取得ロジックの再利用
function useApi(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    async function fetchData() {
      setLoading(true);
      setError(null);
      
      try {
        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);
      }
    }
    
    if (url) {
      fetchData();
    }
  }, [url]);
  
  return { data, loading, error };
}

// カスタムフック:ローカルストレージの管理
function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error('Error reading localStorage:', error);
      return initialValue;
    }
  });
  
  const setValue = (value) => {
    try {
      setStoredValue(value);
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch (error) {
      console.error('Error setting localStorage:', error);
    }
  };
  
  return [storedValue, setValue];
}

// カスタムフックの使用例
function TodoApp() {
  const [todos, setTodos] = useLocalStorage('todos', []);
  const { data: users, loading, error } = useApi('/api/users');
  
  return (
    <div>
      {/* ToDoアプリのコンポーネント */}
    </div>
  );
}

カスタムフックを使うと、共通の処理を再利用できます。 useで始まる関数名にするのが慣習です。

4.3 コンテキスト(Context)による状態共有

深いコンポーネント階層で状態を共有するための仕組みです。

import { createContext, useContext, useReducer } from 'react';

// テーマコンテキストの作成
const ThemeContext = createContext();

// テーマプロバイダー
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  
  const toggleTheme = () => {
    setTheme(theme === 'light' ? 'dark' : 'light');
  };
  
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// ユーザーコンテキスト(より複雑な例)
const UserContext = createContext();

function userReducer(state, action) {
  switch (action.type) {
    case 'LOGIN':
      return { ...state, user: action.payload, isAuthenticated: true };
    case 'LOGOUT':
      return { ...state, user: null, isAuthenticated: false };
    case 'UPDATE_PROFILE':
      return { ...state, user: { ...state.user, ...action.payload } };
    default:
      return state;
  }
}

function UserProvider({ children }) {
  const [state, dispatch] = useReducer(userReducer, {
    user: null,
    isAuthenticated: false
  });
  
  const login = async (credentials) => {
    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(credentials)
      });
      const userData = await response.json();
      dispatch({ type: 'LOGIN', payload: userData });
    } catch (error) {
      console.error('Login failed:', error);
    }
  };
  
  const logout = () => {
    dispatch({ type: 'LOGOUT' });
  };
  
  const updateProfile = (profileData) => {
    dispatch({ type: 'UPDATE_PROFILE', payload: profileData });
  };
  
  return (
    <UserContext.Provider value={{
      ...state,
      login,
      logout,
      updateProfile
    }}>
      {children}
    </UserContext.Provider>
  );
}

// カスタムフックでコンテキストを使いやすく
function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

function useUser() {
  const context = useContext(UserContext);
  if (!context) {
    throw new Error('useUser must be used within a UserProvider');
  }
  return context;
}

// コンテキストを使用するコンポーネント
function Header() {
  const { theme, toggleTheme } = useTheme();
  const { user, logout } = useUser();
  
  return (
    <header className={`header header--${theme}`}>
      <h1>マイアプリ</h1>
      <div>
        <button onClick={toggleTheme}>
          {theme === 'light' ? '🌙' : '☀️'}
        </button>
        {user && (
          <div>
            <span>こんにちは、{user.name}さん</span>
            <button onClick={logout}>ログアウト</button>
          </div>
        )}
      </div>
    </header>
  );
}

// アプリ全体
function App() {
  return (
    <UserProvider>
      <ThemeProvider>
        <Header />
        <main>
          {/* その他のコンポーネント */}
        </main>
      </ThemeProvider>
    </UserProvider>
  );
}

コンテキストを使うと、深いコンポーネント階層でも状態を共有できます。 ただし、使いすぎると複雑になるので注意が必要です。

よくあるつまずきポイントと対策

1. JavaScriptとReactの違いに混乱

問題

// ❌ 直接DOMを操作しようとする
function BadComponent() {
  const handleClick = () => {
    document.getElementById('output').textContent = 'クリックされました';
  };
  
  return (
    <div>
      <button onClick={handleClick}>クリック</button>
      <div id="output"></div>
    </div>
  );
}

ReactでDOM操作を直接行うのは、間違いです。 Reactの状態管理の仕組みを無視してしまいます。

解決策

// ✅ Reactの状態管理を使用
function GoodComponent() {
  const [message, setMessage] = useState('');
  
  const handleClick = () => {
    setMessage('クリックされました');
  };
  
  return (
    <div>
      <button onClick={handleClick}>クリック</button>
      <div>{message}</div>
    </div>
  );
}

Reactでは状態管理を使って、UIを更新します。 状態が変わると、自動的にUIも更新されます。

2. 状態の直接変更

問題

// ❌ 状態を直接変更
function BadTodoList() {
  const [todos, setTodos] = useState([]);
  
  const addTodo = (text) => {
    todos.push({ id: Date.now(), text }); // ❌ 直接変更
    setTodos(todos); // 再レンダリングされない
  };
  
  const toggleTodo = (id) => {
    const todo = todos.find(t => t.id === id);
    todo.completed = !todo.completed; // ❌ 直接変更
    setTodos(todos);
  };
}

状態を直接変更すると、Reactが変更を検知できません。 再レンダリングが起こらないので、UIが更新されません。

解決策

// ✅ イミューダブルな更新
function GoodTodoList() {
  const [todos, setTodos] = useState([]);
  
  const addTodo = (text) => {
    setTodos([...todos, { id: Date.now(), text }]); // ✅ 新しい配列
  };
  
  const toggleTodo = (id) => {
    setTodos(todos.map(todo => 
      todo.id === id 
        ? { ...todo, completed: !todo.completed } // ✅ 新しいオブジェクト
        : todo
    ));
  };
}

スプレッド演算子を使って、新しい配列やオブジェクトを作ります。 これで、Reactが変更を検知できるようになります。

3. useEffectの依存配列の理解不足

問題

// ❌ 依存配列を正しく設定していない
function BadComponent({ userId }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, []); // ❌ userIdが変更されても実行されない
}

依存配列が空だと、コンポーネントのマウント時にしか実行されません。 userIdが変更されても、新しいユーザーデータを取得しません。

解決策

// ✅ 正しい依存配列
function GoodComponent({ userId }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]); // ✅ userIdを依存配列に含める
}

依存配列にuserIdを含めることで、値が変更された時に実行されます。 これで、正しくユーザーデータを取得できます。

まとめ:JavaScriptからReactへの成功への道筋

この記事では、JavaScript経験者がReactへスムーズに移行するためのロードマップを詳しく解説しました。

学習の要点

Phase別の重要ポイント

Phase 1: JavaScript知識の整理・補強

  • ES6+ 文法の習得
  • 配列メソッドの活用
  • 非同期処理とモジュール

Phase 2: React基本概念の習得

  • JSXとコンポーネント
  • StateとProps
  • イベントハンドリング

Phase 3: 実践プロジェクト開発

  • ToDoアプリで基本を理解
  • 天気アプリでAPI連携
  • ショッピングカートで複雑な状態管理

Phase 4: 応用技術の習得

  • useEffectによる副作用管理
  • カスタムフックの作成
  • Contextによる状態共有

成功のカギ

  1. 段階的な学習
  2. 実践重視
  3. JavaScript知識の活用
  4. つまずきポイントの理解

今日から始められること

  1. JavaScript知識の確認
  2. React環境の構築
  3. 簡単なコンポーネント作成
  4. ToDoアプリの開発開始

JavaScriptの知識があれば、Reactの習得は決して難しくありません。 正しいロードマップに従って、着実にステップアップしていきましょう。

あなたのReact学習が成功することを心から願っています。 今日から、モダンなフロントエンド開発者への道を歩み始めませんか?

関連記事