1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

はじめに

Reactコンポーネントは、作成から破棄までの間に様々な段階を経ます。この一連の流れを「ライフサイクル」と呼びます。本記事では、Reactコンポーネントのライフサイクルについて、初心者の方にも分かりやすく解説します。

ライフサイクルの3つのフェーズ

Reactコンポーネントのライフサイクルは、大きく3つのフェーズに分けられます。

┌─────────┐     ┌─────────┐     ┌─────────┐
│  作成    │ --> │  更新    │ --> │  破棄   │
└─────────┘     └─────────┘     └─────────┘

フェーズ1: マウント(Mounting)

コンポーネントが初めてDOMに追加される段階です。

実行されるメソッド(順番):

  1. constructor()
  2. getDerivedStateFromProps()
  3. render()
  4. componentDidMount()

フェーズ2: 更新(Updating)

propsやstateの変更により、コンポーネントが再レンダリングされる段階です。

実行されるメソッド(順番):

  1. getDerivedStateFromProps()
  2. shouldComponentUpdate()
  3. render()
  4. getSnapshotBeforeUpdate()
  5. componentDidUpdate()

フェーズ3: アンマウント(Unmounting)

コンポーネントがDOMから削除される段階です。

実行されるメソッド:

  • componentWillUnmount()

モダンなReact: 関数コンポーネントとHooks

現在のReactでは、関数コンポーネントとHooksを使用することが推奨されています。以下、TypeScriptで実装例を見ていきます。

useEffectによるライフサイクル管理

import { useEffect, useState } from 'react';

interface UserProfileProps {
  userId: string;
}

interface User {
  id: string;
  name: string;
  email: string;
}

const UserProfile: React.FC<UserProfileProps> = ({ userId }) => {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState<boolean>(true);

  // componentDidMount と componentDidUpdate に相当
  useEffect(() => {
    console.log('コンポーネントがマウントまたは更新されました');
    
    // データの取得
    const fetchUser = async () => {
      setLoading(true);
      try {
        const response = await fetch(`/api/users/${userId}`);
        const data = await response.json();
        setUser(data);
      } catch (error) {
        console.error('ユーザー情報の取得に失敗しました', error);
      } finally {
        setLoading(false);
      }
    };

    fetchUser();

    // componentWillUnmount に相当(クリーンアップ関数)
    return () => {
      console.log('コンポーネントがアンマウントされます');
      // タイマーのクリアやイベントリスナーの削除など
    };
  }, [userId]); // 依存配列: userIdが変更された時のみ実行

  if (loading) return <div>読み込み中...</div>;
  if (!user) return <div>ユーザーが見つかりません</div>;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
};

ライフサイクルメソッドとHooksの対応表

クラスコンポーネント Hooks 用途
constructor useState 初期状態の設定
componentDidMount useEffect(() => {}, []) 初回レンダリング後の処理
componentDidUpdate useEffect(() => {}) 更新後の処理
componentWillUnmount useEffect(() => { return () => {} }, []) クリーンアップ処理
shouldComponentUpdate React.memo, useMemo レンダリングの最適化

ライフサイクルフローチャート

マウント時:
┌──────────────┐
│ constructor  │
└──────┬───────┘
       ↓
┌──────────────────────────┐
│ getDerivedStateFromProps │
└──────────┬───────────────┘
           ↓
┌──────────────┐
│    render    │
└──────┬───────┘
       ↓
┌───────────────────┐
│ componentDidMount │
└───────────────────┘

更新時:
┌──────────────────────────┐
│ getDerivedStateFromProps │
└──────────┬───────────────┘
           ↓
┌────────────────────────┐
│ shouldComponentUpdate  │
└──────────┬─────────────┘
           ↓
┌──────────────┐
│    render    │
└──────┬───────┘
       ↓
┌───────────────────────────┐
│ getSnapshotBeforeUpdate   │
└───────────┬───────────────┘
            ↓
┌────────────────────┐
│ componentDidUpdate │
└────────────────────┘

アンマウント時:
┌──────────────────────┐
│ componentWillUnmount │
└──────────────────────┘

実践的な使用例

1. タイマーの管理

import { useEffect, useState } from 'react';

const Timer: React.FC = () => {
  const [seconds, setSeconds] = useState<number>(0);

  useEffect(() => {
    // タイマーの設定
    const interval = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);

    // クリーンアップ関数でタイマーをクリア
    return () => {
      clearInterval(interval);
    };
  }, []); // 空の依存配列により、マウント時のみ実行

  return <div>経過時間: {seconds}</div>;
};

2. イベントリスナーの管理

import { useEffect, useState } from 'react';

const WindowSize: React.FC = () => {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    };

    // イベントリスナーの登録
    window.addEventListener('resize', handleResize);

    // クリーンアップ関数でイベントリスナーを削除
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  return (
    <div>
      ウィンドウサイズ: {windowSize.width} x {windowSize.height}
    </div>
  );
};

3. APIコールとデータフェッチング

import { useEffect, useState } from 'react';

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

const PostList: React.FC = () => {
  const [posts, setPosts] = useState<Post[]>([]);
  const [loading, setLoading] = useState<boolean>(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let cancelled = false;

    const fetchPosts = async () => {
      try {
        setLoading(true);
        const response = await fetch('/api/posts');
        
        if (!response.ok) {
          throw new Error('データの取得に失敗しました');
        }
        
        const data = await response.json();
        
        // コンポーネントがアンマウントされていない場合のみ更新
        if (!cancelled) {
          setPosts(data);
          setError(null);
        }
      } catch (err) {
        if (!cancelled) {
          setError(err instanceof Error ? err.message : '予期しないエラー');
        }
      } finally {
        if (!cancelled) {
          setLoading(false);
        }
      }
    };

    fetchPosts();

    // クリーンアップ関数
    return () => {
      cancelled = true;
    };
  }, []);

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

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>
          <h3>{post.title}</h3>
          <p>{post.body}</p>
        </li>
      ))}
    </ul>
  );
};

パフォーマンス最適化のベストプラクティス

1. 不要な再レンダリングを避ける

import { useMemo, useCallback } from 'react';

interface ExpensiveComponentProps {
  data: number[];
  multiplier: number;
}

const ExpensiveComponent: React.FC<ExpensiveComponentProps> = ({ data, multiplier }) => {
  // 計算結果をメモ化
  const processedData = useMemo(() => {
    console.log('重い計算を実行中...');
    return data.map(item => item * multiplier);
  }, [data, multiplier]);

  // 関数をメモ化
  const handleClick = useCallback(() => {
    console.log('クリックされました');
  }, []);

  return (
    <div>
      <button onClick={handleClick}>クリック</button>
      <ul>
        {processedData.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </div>
  );
};

2. 条件付きエフェクトの実行

import { useEffect, useRef } from 'react';

interface ConditionalEffectProps {
  shouldFetch: boolean;
  query: string;
}

const ConditionalEffect: React.FC<ConditionalEffectProps> = ({ shouldFetch, query }) => {
  const prevQueryRef = useRef<string>('');

  useEffect(() => {
    // 条件に基づいてエフェクトを実行
    if (shouldFetch && query !== prevQueryRef.current) {
      console.log(`検索実行: ${query}`);
      // APIリクエストなどの処理
      prevQueryRef.current = query;
    }
  }, [shouldFetch, query]);

  return <div>検索クエリ: {query}</div>;
};

3. React.memoによるコンポーネントの最適化

import React, { memo } from 'react';

interface ChildComponentProps {
  name: string;
  age: number;
}

// メモ化されたコンポーネント
const ChildComponent = memo<ChildComponentProps>(({ name, age }) => {
  console.log('ChildComponent がレンダリングされました');
  return (
    <div>
      <p>名前: {name}</p>
      <p>年齢: {age}</p>
    </div>
  );
});

// カスタム比較関数を使用する場合
const OptimizedChild = memo<ChildComponentProps>(
  ({ name, age }) => {
    return (
      <div>
        <p>名前: {name}</p>
        <p>年齢: {age}</p>
      </div>
    );
  },
  (prevProps, nextProps) => {
    // trueを返すとレンダリングをスキップ
    return prevProps.name === nextProps.name && 
           prevProps.age === nextProps.age;
  }
);

よくあるアンチパターンと解決策

アンチパターン1: 加工した値をstateに保存

// ❌ 悪い例
const BadComponent: React.FC<{ date: Date }> = ({ date }) => {
  const [day, setDay] = useState(date.getDay());
  
  // propsが変更されてもdayは更新されない
  return <div>曜日: {day}</div>;
};

// ✅ 良い例
const GoodComponent: React.FC<{ date: Date }> = ({ date }) => {
  // レンダリング時に計算
  const day = date.getDay();
  
  return <div>曜日: {day}</div>;
};

アンチパターン2: useEffect内での無限ループ

// ❌ 悪い例
const BadInfiniteLoop: React.FC = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // countが更新されるたびに実行され、無限ループになる
    setCount(count + 1);
  }); // 依存配列を指定していない

  return <div>{count}</div>;
};

// ✅ 良い例
const GoodExample: React.FC = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // マウント時に一度だけ実行
    setCount(prevCount => prevCount + 1);
  }, []); // 空の依存配列

  return <div>{count}</div>;
};

アンチパターン3: クリーンアップの忘れ

// ❌ 悪い例
const BadCleanup: React.FC = () => {
  useEffect(() => {
    const timer = setInterval(() => {
      console.log('タイマー実行');
    }, 1000);
    // クリーンアップ関数がない!
  }, []);

  return <div>タイマーコンポーネント</div>;
};

// ✅ 良い例
const GoodCleanup: React.FC = () => {
  useEffect(() => {
    const timer = setInterval(() => {
      console.log('タイマー実行');
    }, 1000);

    // クリーンアップ関数でタイマーをクリア
    return () => {
      clearInterval(timer);
    };
  }, []);

  return <div>タイマーコンポーネント</div>;
};

useEffectの依存配列パターン

// パターン1: 空の依存配列 - マウント時のみ実行
useEffect(() => {
  console.log('マウント時のみ実行');
}, []);

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

// パターン3: 特定の値に依存 - 値が変更された時のみ実行
useEffect(() => {
  console.log('valueが変更された時に実行');
}, [value]);

// パターン4: 複数の依存関係
useEffect(() => {
  console.log('value1またはvalue2が変更された時に実行');
}, [value1, value2]);

カスタムHookでライフサイクルロジックの再利用

// カスタムHook: ウィンドウサイズを追跡
function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    const handleResize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    };

    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return size;
}

// カスタムHook: ローカルストレージの同期
function useLocalStorage<T>(key: string, initialValue: T) {
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(error);
      return initialValue;
    }
  });

  const setValue = (value: T | ((val: T) => T)) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(error);
    }
  };

  return [storedValue, setValue] as const;
}

// 使用例
const Component: React.FC = () => {
  const windowSize = useWindowSize();
  const [theme, setTheme] = useLocalStorage('theme', 'light');

  return (
    <div>
      <p>ウィンドウサイズ: {windowSize.width} x {windowSize.height}</p>
      <p>テーマ: {theme}</p>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        テーマ切り替え
      </button>
    </div>
  );
};

まとめ

Reactコンポーネントのライフサイクルを理解することは、効率的なアプリケーション開発に不可欠です。主なポイントは以下の通りです。

  1. ライフサイクルは3つのフェーズ(マウント、更新、アンマウント)で構成されます
  2. モダンなReactではHooksを使用して、より簡潔にライフサイクルを管理できます
  3. useEffectは万能で、ほとんどのライフサイクル処理をカバーできます
  4. クリーンアップは重要で、メモリリークを防ぐために必ず実装します
  5. パフォーマンス最適化には、useMemo、useCallback、React.memoを適切に使用します
  6. 依存配列の理解が、正しいエフェクトの実行タイミングの制御に不可欠です
  7. カスタムHookにより、ライフサイクルロジックを再利用可能にできます

これらの概念を理解し、適切に活用することで、保守性が高く、パフォーマンスの良いReactアプリケーションを開発できます。特に、関数コンポーネントとHooksを使用した現代的なアプローチは、コードをより簡潔で理解しやすくし、テストもしやすくなります。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?