React 応用①

useEffect、SPA

前回の復習

React基礎②のおさらい

JSX = JavaScript + HTML風の文法

// 変数の表示
const name = "Ichigo";
<h1>Hello, {name}!</h1>

// 条件表示
{isLoggedIn ? <Profile /> : <Login />}
{user.isAdmin && <AdminPanel />}

// リスト表示  
{todos.map(todo => 
  <li key={todo.id}>{todo.text}</li>
)}
{}でJavaScript、keyは必須

コンポーネント = 関数

// コンポーネント定義(大文字で開始)
function UserCard({ name, email, children }) {
  return (
    <div className="card">
      <h3>{name}</h3>
      <p>{email}</p>
      {children}
    </div>
  );
}

// 使用
<UserCard name="Kurotsuchi Mayuri" email="m.kurotsuchi@seireitei.co.jp">
  <button>編集</button>
</UserCard>
props = 関数の引数、children = タグの中身

イベント処理 = ユーザー操作に応じて関数を実行

イベントハンドラーはJSXの属性として指定されます

// ボタンクリックの基本
const handleClick = () => {
  alert('押したよ!');
};

return <button onClick={handleClick}>押す</button>;

// 複数ボタン + データ渡し
const characters = ['Rena', 'Satoko', 'Rika'];
return (
  <div>
    {characters.map(name => (
      <button key={name} onClick={() => handleSelect(name)}>
        {name}
      </button>
    ))}
  </div>
);
イベントハンドラーは関数を直接渡す、アロー関数で引数を渡すことも可能

状態 = 再レンダーを引き起こす変数

import { useState } from 'react'; // フックをインポート
// 基本構文
const [likes, setLikes] = useState(0); // 初期値は0

// 数値の更新
setLikes(likes + 1);           // 現在の値を使って直接更新
setLikes(prev => prev + 1);    // 前の値を使って関数で安全に更新
// 配列の分割代入なし版
const likesState = useState(0); 

// likesState[0] = 現在値
// likesState[1] = セッター関数
likesState[1](likesState[0] + 1);

// オブジェクトの更新
const [user, setUser] = useState({ name: 'Ichigo', age: 17 });
setUser({...user, age: 18}); // スプレッド構文で既存保持

// 配列の更新  
const [items, setItems] = useState(['bat', 'knife']);
setItems([...items, 'nata']); // 新しい配列を作成
必ずセッター関数を使用、オブジェクト/配列は新しく作成

制御されたコンポーネント = 状態 + イベントハンドラー


基本構造

// 3つの要素が必須
const [value, setValue] = useState('');        // 1. 状態

return (
  <input 
    value={value}                              // 2. input要素
    onChange={(e) => setValue(e.target.value)} // 3. ハンドラー関数
  />
);

重要なポイント

  • valueは必ずstate由来
  • onChangeでstateを更新
  • 状態変更 → 再レンダリング
  • Reactが完全に制御
・「Controlled」では、Reactが扱います
・「Uncontrolled」では、Reactが状態を知らない、DOMに任せます

シングルページアプリ【SPA】

SPAとは、従来のWebアプリとの違い

SPAの基本概念

シングルページアプリケーション = 単一HTMLファイル

  • アプリケーション全体が1つのindex.htmlから始まる
  • ページ遷移時も同じHTMLファイル内で完結
  • JavaScriptがDOMを直接操作してコンテンツを切り替える
  • URLが変わる場合もあるが、実際にはページリロードしない


<!DOCTYPE html>
<html>
<head>
    <title>React App</title>
</head>
<body>
    <div id="root"></div> <!-- ここにReactがコンテンツを描画 -->
    <script src="bundle.js"></script>
</body>
</html>

SPAのファイル構造イメージ

従来のWebサイト


website/
├── index.html      ← ホーム
├── about.html      ← 概要ページ  
├── contact.html    ← お問い合わせ
├── products.html   ← 商品一覧
└── ...

各ページ = 別々のHTMLファイル

SPA


react-app/
├── index.html      ← これ1つだけ!
├── app.jsx         ← React アプリケーション
├── styles.css
├── components/
└── ...

全ページ = 1つのHTMLファイル内で切り替え

従来のWebアプリ vs SPA

マルチページアプリ【MPA】

複数のHTMLファイル

  1. ユーザーがリンクをクリック
  2. ブラウザが新しいHTMLファイルを要求
  3. サーバーが完全な別ページを送信
  4. ブラウザが古いページを破棄
  5. 新しいページを描画
  6. 画面のフラッシュ/再読み込み

シングルページアプリ【SPA】

単一のHTMLファイル

  1. ユーザーがリンクをクリック
  2. JavaScriptがクリックを検知
  3. 必要なデータのみをAPI経由で取得
  4. Reactが同じページ内のDOMで変更部分のみを更新
  5. 瞬間的なページ遷移

コード例:ページ遷移の違い

従来のWebアプリ

ブラウザがHTMLファイルを切り替え


<!-- about.html という別ファイルに移動 -->
<a href="/about.html">概要</a>

<!-- contact.html という別ファイルに移動 -->
<a href="/contact.html">お問い合わせ</a>

SPA

JavaScriptでコンテンツを切り替え


// 表示するコンテンツを状態で管理
const [currentPage, setCurrentPage] = useState('home');

const showAbout = () => {
  setCurrentPage('about'); // DOMを書き換え
}

// 条件分岐でコンテンツを切り替え
{currentPage === 'home' && <HomeContent />}
{currentPage === 'about' && <AboutContent />}

実際にはルーティングライブラリを使用することが多い

SPAの注意点

メリット

  • 高速なページ遷移
  • アプリ風UX
  • サーバー負荷軽減

懸念点

  • 初回ロードが重い
  • SEOが困難
  • JavaScriptが必須

SPAの根本的な問題

純粋なSPAは多くの問題を抱えており現在は「呪われた」技術とされることも

主な問題点

  • 初回ロード : すべてのJavaScriptをダウンロードするまで何も表示されない
  • SEO : 検索エンジンがJavaScriptを実行しないと内容が見えない
  • パフォーマンス : バンドルが大きくなりがち
  • 複雑性 : クライアントサイドでの状態管理が複雑
SPAが呪われた技術

【念のため】現実的な解決策

SPAの問題を解決するレンダリング戦略

  • SSR【Server Side Rendering】
    サーバーでHTMLを生成
  • SSG【Static Site Generation】
    ビルド時に静的HTMLを生成
  • ISR【Incremental Static Regeneration】
    必要に応じて静的ページを再生成
純粋SPAの問題を解決するためのフレームワーク:
Next.js、React Router/Remix、Astro、Gatsbyなど

まとめ:SPA

  • 構造: 単一HTMLファイル、JSでDOMを操作し、リロードなしで表示更新
  • 体験: 高速な遷移、アプリ風UX
  • 課題: 初回ロード重い、SEOに弱い
  • 実践解: フレームワークでレンダリング戦略を補完

Reactのライフサイクル

コンポーネントの状態管理と描画サイクルを理解する

コンポーネントのライフサイクル

すべてのコンポーネントは3つの段階を経ます

マウント

コンポーネントが作成される

  • 初期state・propsの設定
  • JSXから仮想DOMの生成
  • 実DOMへの挿入
  • レイアウト・ペイント

更新

コンポーネントが変更される

  • stateやpropsの変更検知
  • 仮想DOMの再生成
  • 差分計算(Reconciliation)
  • 実DOMの更新

アンマウント

コンポーネントが破棄される

  • 実DOMからの削除
  • イベントリスナーの解除
  • メモリの解放

純粋コンポーネント

Reactの基本原則:同じ入力には同じ出力


純粋関数の特徴

  • 同じpropsで常に同じJSXを返す
  • レンダリング中に外部状態を変更しない
  • 外部の変数に依存しない

純粋なコンポーネント

function PureComponent({ name }) {
  return (
        <h1>こんにちは、{name}さん!</h1>
  );
}

不純なコンポーネント

function ImpureComponent({ name }) {
  document.title = `Welcome ${name}`; // 副作用
  
  return <h1>こんにちは、{Math.random()}さん!</h1>; // 非決定的
}

副作用(Side Effect)とは

コンポーネントの純粋性を破る操作、でも現実的なアプリには不可欠

データ操作

  • API呼び出し
  • データベース操作、ローカルストレージ
  • I/O操作

DOM操作

  • 要素への直接アクセス
  • フォーカス制御
  • スクロール操作

購読・解除

  • イベントリスナー
  • WebSocket接続
  • タイマー設定

外部ライブラリ

  • チャートライブラリ
  • アニメーションライブラリ
  • マップライブラリ
レンダリング中の副作用は予期しない動作を引き起こす
例:無限ループ、予期しない再レンダリング

副作用はいつ実行できる?

Reactの実行フェーズを理解する

レンダーフェーズ

JSX → 仮想DOM

コンポーネントが何を表示するかを「計算」する段階

  • 前回との差分を計算(reconciliation)
  • 実際のDOMはまだ変更されない
  • 中断・再実行される可能性
副作用禁止

コミットフェーズ

仮想DOM → 実DOM

実際にDOMを「更新」する段階

  • 計算された差分を実DOMに適用
  • 副作用を安全に実行可能
  • 安定した実行環境
副作用OK

Reactで副作用を実行する方法

副作用のタイミングによって使い分ける

ユーザーアクション時

イベントハンドラー

レンダリングサイクル外で実行

  • ボタンクリック、フォーム送信
  • ユーザー操作が起点


function Button() {
  function handleClick() {
    fetch('/api/data'); // 副作用OK
  }
  
  return <button onClick={handleClick}>受信する</button>;
}

コンポーネント状態変化時

useEffect

コミットフェーズに実行

  • データ取得、購読・解除
  • 自動実行が必要


function Profile({ userId }) {
  useEffect(() => { // userIdが変わったら自動でデータ取得
    fetch(`/api/users/${userId}`);
  }, [userId]);

  return <>...<>;
}

useEffectの基本形

useEffect(setup, dependencies?)

setup関数

  • 副作用を実行する関数
  • 必須パラメータ
  • クリーンアップ関数を返せる

dependencies配列

  • 再実行の条件を指定
  • オプション(省略可能)
  • 値の変更を監視
import { useEffect } from 'react';

function MyComponent() {
  function setup() {
    console.log('副作用を実行');
  }

  useEffect(setup);

  return <div>...</div>;
}

useEffectの基本構文

import { useEffect } from "react";

function Component() {
    function cleanup() { // クリーンアップ関数:リソースの解放
        console.log("アンマウント時や次のエフェクト実行前");
    }

    function setup() { // エフェクト関数:副作用を実行
        console.log("DOM更新後に実行される");
        return cleanup;
    }

    useEffect(setup, [/* 依存配列 */]);

    return <div>...</div>;
}
エフェクト関数
副作用を実行する関数
クリーンアップ関数
リソース解放(オプション)
依存配列
参照する値の一覧(オプション)

依存配列による実行制御

  1. 毎回実行

    useEffect(() => {
      console.log('毎回のレンダリング後に実行');
    }); // 依存配列なし
  2. 初回のみ実行

    useEffect(() => {
      console.log('マウント時のみ実行');
    }, []); // 空の依存配列
  3. 条件付き実行

    useEffect(() => {
      console.log('userIdが変わった時のみ実行');
    }, [userId]); // userIdを監視して、変わったら実行

useEffectの実行フロー

import { useEffect } from "react";

function Component() {
  useEffect(
    () => { // エフェクト関数:副作用を実行
      console.log("DOM更新後に実行される");

      return () => { // クリーンアップ関数:リソースの解放
        console.log("アンマウント時や次のエフェクト実行前");
      };
    },
    [/* 依存配列 */]
  );

  return <div>...</div>;
}

1. レンダリング完了

コンポーネントのJSXが仮想DOMに変換され、実DOMが更新される

2. 依存配列チェック

前回の値と比較し、変更があるかを確認

  • 初回実行時:常に実行
  • 再レンダリング時:依存値の変更時のみ

3. クリーンアップ実行

前回のエフェクトのクリーンアップ関数(ある場合)

4. 新しいエフェクト実行

セットアップ関数が実行され、副作用処理を行う

実践例:ポケットモンデータの取得

useEffectの動作フロー

初回表示時

  1. コンポーネントマウント
    selectedPokemon="jigglypuff"PokemonCardが初回レンダリング
    <PokemonCard name="jigglypuff" />
  2. useEffect実行
    レンダリング完了後、useEffectが実行される
    loading: truefetchでデータ取得開始
    useEffect(() => {
      setLoading(true);
      fetch(`https://pokeapi.co/api/v2/pokemon/jigglypuff`)
      ...
    
  3. データ取得と状態更新
    loading: true → falsepokemon: null → データ
    
    .then((pokemonData) => {
      setPokemon(pokemonData);
      setLoading(false);
    });

ボタンクリック時

  1. state変更
    selectedPokemon: "jigglypuff" → "pikachu"
    <button onClick={() => setSelectedPokemon("pikachu")}>
  2. propsの変更検知
    name: "jigglypuff" → "pikachu"
    <PokemonCard name="pikachu" /> // propsが変更された
  3. useEffect再実行
    依存配列[name]の値が変わったため、useEffectが再度実行される
    }, [name]); // "pikachu"に変更されたので再実行
  4. 新しいデータ取得
    ピカチュウのデータを取得し、UIが更新される
    fetch(`https://pokeapi.co/api/v2/pokemon/pikachu`)

useEffectとライフサイクル

マウント

  1. コンポーネント初期化
  2. 初回レンダリング
  3. DOM更新
  4. useEffect実行

更新

  1. stateやpropsの変更検知
  2. 再レンダリング
  3. DOM差分更新
  4. 前のエフェクトクリーンアップ
  5. 新しいエフェクト実行

アンマウント

  1. 全エフェクトクリーンアップ
  2. DOMから削除
  3. メモリ解放

ビジュアル解説はこちら

よくあるミス

  • 依存配列の漏れ

    useEffect(() => {
      fetchData(userId); // userIdに依存しているが...
    }, []); // 依存配列に含まれていない
    対策: ESLint plugin-react-hooksを使用
  • 無限ループ

    useEffect(() => {
        setCount(count + 1); // countに依存
    }, [count]); // 無限ループ発生

    対策: 関数型更新を使用: setCount(c => c + 1)

  • クリーンアップ忘れ

    useEffect(() => {
      const timer = setInterval(updateData, 1000);
      // クリーンアップなし → メモリリーク
    }, []);

    対策: 必ずクリーンアップ関数でリソース解放

開発環境での注意点

ReactのStrict Mode

開発環境

  • エフェクトが2回実行される
  • 意図的な動作
  • バグの早期発見

本番環境

  • 通常通り1回のみ実行
  • 最適化された動作
  • パフォーマンス重視
重要: 開発環境で2回実行されても正常に動作するようにクリーンアップを実装する

useLayoutEffectとの違い

useEffect

  • ブラウザの描画後に実行
  • 非同期実行
  • 一般的な副作用に使用
  • 推奨:ほとんどのケース

useLayoutEffect

  • ブラウザの描画前に実行
  • 同期実行
  • DOM測定・操作に使用
  • 使用場面:レイアウト計算時
// DOM要素のサイズを測定する場合のみ
useLayoutEffect(() => {
  const rect = ref.current.getBoundingClientRect();
  setDimensions({ width: rect.width, height: rect.height });
}, []);

99%の場合はuseEffectで十分

まとめ

純粋性を保つ
レンダリング中は副作用を避ける
適切なタイミング
useEffectで副作用を安全に実行
依存関係の管理
依存配列で実行タイミングを制御
リソースの解放
クリーンアップでメモリリーク防止

理解度チェック①【ライフサイクル】

Reactコンポーネントの正しいライフサイクルの順序は?


A) 更新 → マウント → アンマウント
B) マウント → 更新 → アンマウント
C) マウント → アンマウント → 更新
D) アンマウント → マウント → 更新
答え: B) コンポーネントは作成(マウント)→変更(更新)→破棄(アンマウント)の順

理解度チェック②【純粋コンポーネント】

どのコンポーネントが純粋?


function ComponentA({ name }) { // A
  console.log('Rendering:', name);
  return <h1>Hello {name}</h1>;
}

function ComponentB({ name }) { // B  
  return <h1>Hello {name}</h1>;
}

function ComponentC({ name }) { // C
  document.title = name;
  return <h1>Hello {name}</h1>;
}
A) ComponentA(ログ出力するから)
B) ComponentB(副作用なし)
C) ComponentC(タイトル更新するから)
D) どれも純粋ではない
答え: B) 同じpropsで常に同じJSXを返し、副作用がない

理解度チェック③【依存配列】

このuseEffectはいつ実行される?

const [count, setCount] = useState(0);
const [name, setName] = useState('');

useEffect(() => {
  console.log('Effect runs');
}, [count]);
A) 毎回のレンダリング後
B) countが変更された時のみ
C) nameが変更された時のみ
D) マウント時のみ
答え: B) 依存配列に含まれるcountが変更された時のみ実行

理解度チェック④【クリーンアップ】

このコードの問題点は?

function UserStatus({ userId }) {
  const [status, setStatus] = useState('offline');
  
  useEffect(() => {
    function handleStatusChange(newStatus) {
      setStatus(newStatus);
    }
    
    StatusAPI.subscribe(userId, handleStatusChange);
  }, [userId]);
  
  return <div>Status: {status}</div>;
}
A) useEffectの使い方が間違い
B) 依存配列にuserIdが含まれている
C) サブスクリプションが解除されない
D) 特に問題なし
答え: C) アンマウント時や次のエフェクト前にunsubscribeが必要

React 実践演習

1つの課題 (15-20分)

今日学んだ内容を実際に使ってみよう!

課題1 斬魄刀ロアダー

<ZanpakutoLoader> コンポーネントを作成してください

  • ローディング中は「読み込み中...」と表示
  • 成功時は namepowerLevel を表示
  • 失敗時はエラーメッセージを表示
  • useStateuseEffect を組み合わせて実装すること
function ZanpakutoLoader({ name }: { name: string }) {
  const [zanpakuto, setZanpakuto] = useState<Zanpakuto | null>(null);
  const [error, setError] = useState("");
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    // name が変更された時に loadZanpakto を呼び出す処理を書く
  }, [name]);

  return <div>{/* ローディング、結果、エラーの表示 */}</div>;
}

課題1 斬魄刀ロアダー

第3回まとめ

今日学んだこと

  • SPAとは、従来のWebアプリとの違い
  • コンポーネントのライフサイクル
  • 副作用と純粋コンポーネントの概念
  • useEffectの基本的な使い方

次回予告

React④: カスタムフック、共通化、振り返り