React Hooksとは?クラスコンポーネントから卒業する理由

React Hooksの基本概念から実際の使い方まで解説。クラスコンポーネントからの移行理由と具体的な変更方法を実例とともに詳しく説明します。

みなさん、React開発でこんな疑問を持ったことはありませんか?

「React Hooksって聞いたことあるけど、何?」
「クラスコンポーネントで十分動いてるのに、変える必要ある?」
「移行したいけど、どこから始めればいいかわからない」

React Hooksは、Reactの書き方を大きく変えた画期的な機能です。 でも、なぜこんなに注目されているのでしょうか?

この記事では、React Hooksの基本から移行のメリットまで、わかりやすく解説します。 きっと「Hooksを使ってみたい!」と思えるようになりますよ。

React Hooksって何?基本を理解しよう

React Hooksは、関数コンポーネントでstateやライフサイクルを扱える仕組みです。 React 16.8で導入され、今では多くの開発者に愛用されています。

Hooksの基本的な仕組み

React Hooksは「use」で始まる特別な関数です。 これらを使うことで、関数コンポーネントにいろいろな機能を追加できます。

import React, { useState, useEffect } from 'react';

function MyComponent() {
  // useState Hook: データを保存・更新
  const [count, setCount] = useState(0);
  
  // useEffect Hook: 画面の更新タイミングで処理を実行
  useEffect(() => {
    document.title = `Count: ${count}`;
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        クリック
      </button>
    </div>
  );
}

このコードを見ると、関数コンポーネントなのにstateが使えていますね。 これがHooksの魔法です!

よく使うHooksの種類

Hooksにはいろいろな種類があります。 まずは基本的なものから覚えていきましょう。

必須で覚えたいHooks

  • useState: データの保存・更新
  • useEffect: 画面更新時の処理
  • useContext: データの共有

慣れてきたら使いたいHooks

  • useReducer: 複雑なデータ管理
  • useCallback: 関数の最適化
  • useMemo: 計算結果の最適化
  • useRef: DOM要素への直接アクセス

自分で作れるHooks

  • カスタムHooks: 便利な機能を再利用可能にする

カスタムHooksの威力

自分でオリジナルのHooksを作ることもできます。

// カスタムHookの例:カウンター機能
function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);
  
  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);
  const reset = () => setCount(initialValue);
  
  return { count, increment, decrement, reset };
}

// 使用例
function CounterComponent() {
  const { count, increment, decrement, reset } = useCounter(10);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

一度作れば、いろいろなコンポーネントで再利用できます。 とても便利ですよね!

クラスコンポーネントと比べてみよう

実際のコードで、クラスコンポーネントとHooksの違いを見てみましょう。 きっと「Hooksの方が簡単!」と感じるはずです。

データ管理の違い

クラスコンポーネント(従来の方法)

import React, { Component } from 'react';

class ClassCounter extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
      name: '',
      isVisible: true
    };
  }

  handleIncrement = () => {
    this.setState({ count: this.state.count + 1 });
  };

  handleNameChange = (e) => {
    this.setState({ name: e.target.value });
  };

  toggleVisibility = () => {
    this.setState({ isVisible: !this.state.isVisible });
  };

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.handleIncrement}>増加</button>
        
        <input
          type="text"
          value={this.state.name}
          onChange={this.handleNameChange}
          placeholder="名前を入力"
        />
        
        <button onClick={this.toggleVisibility}>
          {this.state.isVisible ? '非表示' : '表示'}
        </button>
        
        {this.state.isVisible && <p>こんにちは、{this.state.name}さん</p>}
      </div>
    );
  }
}

Hooks(新しい方法)

import React, { useState } from 'react';

function HooksCounter() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
  const [isVisible, setIsVisible] = useState(true);

  const handleIncrement = () => {
    setCount(count + 1);
  };

  const handleNameChange = (e) => {
    setName(e.target.value);
  };

  const toggleVisibility = () => {
    setIsVisible(!isVisible);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleIncrement}>増加</button>
      
      <input
        type="text"
        value={name}
        onChange={handleNameChange}
        placeholder="名前を入力"
      />
      
      <button onClick={toggleVisibility}>
        {isVisible ? '非表示' : '表示'}
      </button>
      
      {isVisible && <p>こんにちは、{name}さん</p>}
    </div>
  );
}

Hooksの方がスッキリしていて読みやすいですね。 this.statethis.setStateがなくなって、とてもシンプルです。

ライフサイクルの違い

画面の更新タイミングで処理を実行する方法も比較してみましょう。

クラスコンポーネント

class ClassLifecycle extends Component {
  constructor(props) {
    super(props);
    this.state = {
      data: null,
      loading: true,
      error: null
    };
  }

  componentDidMount() {
    this.fetchData();
  }

  componentDidUpdate(prevProps) {
    if (prevProps.userId !== this.props.userId) {
      this.fetchData();
    }
  }

  componentWillUnmount() {
    // クリーンアップ処理
    if (this.abortController) {
      this.abortController.abort();
    }
  }

  fetchData = async () => {
    try {
      this.setState({ loading: true });
      this.abortController = new AbortController();
      
      const response = await fetch(`/api/users/${this.props.userId}`, {
        signal: this.abortController.signal
      });
      const data = await response.json();
      
      this.setState({ data, loading: false });
    } catch (error) {
      if (error.name !== 'AbortError') {
        this.setState({ error: error.message, loading: false });
      }
    }
  };

  render() {
    const { data, loading, error } = this.state;
    
    if (loading) return <div>読み込み中...</div>;
    if (error) return <div>エラー: {error}</div>;
    
    return (
      <div>
        <h2>ユーザー情報</h2>
        <p>名前: {data?.name}</p>
        <p>メール: {data?.email}</p>
      </div>
    );
  }
}

Hooks

import React, { useState, useEffect } from 'react';

function HooksLifecycle({ userId }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const abortController = new AbortController();
    
    const fetchData = async () => {
      try {
        setLoading(true);
        setError(null);
        
        const response = await fetch(`/api/users/${userId}`, {
          signal: abortController.signal
        });
        const userData = await response.json();
        
        setData(userData);
        setLoading(false);
      } catch (error) {
        if (error.name !== 'AbortError') {
          setError(error.message);
          setLoading(false);
        }
      }
    };

    fetchData();

    // クリーンアップ処理
    return () => {
      abortController.abort();
    };
  }, [userId]); // userIdが変更されたときに再実行

  if (loading) return <div>読み込み中...</div>;
  if (error) return <div>エラー: {error}</div>;
  
  return (
    <div>
      <h2>ユーザー情報</h2>
      <p>名前: {data?.name}</p>
      <p>メール: {data?.email}</p>
    </div>
  );
}

useEffectを使うことで、複数のライフサイクルメソッドを1つにまとめられます。 関連する処理が一箇所にまとまって、とても理解しやすいです。

なぜクラスコンポーネントから卒業すべき?

「今のコードで動いているのに、なぜ変える必要があるの?」 そんな疑問にお答えします。

1. コードがスッキリして読みやすい

同じ機能でも、Hooksの方が圧倒的に短くなります。

// クラスコンポーネント: 40行
class ClassTimer extends Component {
  constructor(props) {
    super(props);
    this.state = {
      seconds: 0,
      isRunning: false
    };
  }

  componentDidMount() {
    this.interval = setInterval(() => {
      if (this.state.isRunning) {
        this.setState({ seconds: this.state.seconds + 1 });
      }
    }, 1000);
  }

  componentWillUnmount() {
    clearInterval(this.interval);
  }

  start = () => {
    this.setState({ isRunning: true });
  };

  stop = () => {
    this.setState({ isRunning: false });
  };

  reset = () => {
    this.setState({ seconds: 0, isRunning: false });
  };

  render() {
    return (
      <div>
        <p>時間: {this.state.seconds}秒</p>
        <button onClick={this.start}>開始</button>
        <button onClick={this.stop}>停止</button>
        <button onClick={this.reset}>リセット</button>
      </div>
    );
  }
}

// Hooks: 25行
function HooksTimer() {
  const [seconds, setSeconds] = useState(0);
  const [isRunning, setIsRunning] = useState(false);

  useEffect(() => {
    const interval = setInterval(() => {
      if (isRunning) {
        setSeconds(seconds => seconds + 1);
      }
    }, 1000);

    return () => clearInterval(interval);
  }, [isRunning]);

  return (
    <div>
      <p>時間: {seconds}秒</p>
      <button onClick={() => setIsRunning(true)}>開始</button>
      <button onClick={() => setIsRunning(false)}>停止</button>
      <button onClick={() => { setSeconds(0); setIsRunning(false); }}>リセット</button>
    </div>
  );
}

40行が25行になりました! 15行も短縮できて、読みやすさも格段に向上しています。

2. 機能を簡単に再利用できる

カスタムHooksを使えば、便利な機能を簡単に再利用できます。

// カスタムHook: タイマー機能
function useTimer(initialSeconds = 0) {
  const [seconds, setSeconds] = useState(initialSeconds);
  const [isRunning, setIsRunning] = useState(false);

  useEffect(() => {
    let interval = null;
    if (isRunning) {
      interval = setInterval(() => {
        setSeconds(seconds => seconds + 1);
      }, 1000);
    }
    return () => clearInterval(interval);
  }, [isRunning]);

  const start = () => setIsRunning(true);
  const stop = () => setIsRunning(false);
  const reset = () => {
    setSeconds(initialSeconds);
    setIsRunning(false);
  };

  return { seconds, isRunning, start, stop, reset };
}

// 複数のコンポーネントで再利用
function Timer1() {
  const { seconds, isRunning, start, stop, reset } = useTimer(0);
  
  return (
    <div>
      <h3>タイマー1</h3>
      <p>{seconds}秒</p>
      <button onClick={start}>開始</button>
      <button onClick={stop}>停止</button>
      <button onClick={reset}>リセット</button>
    </div>
  );
}

function Timer2() {
  const { seconds, isRunning, start, stop, reset } = useTimer(100);
  
  return (
    <div>
      <h3>タイマー2(100秒スタート)</h3>
      <p>{seconds}秒</p>
      <button onClick={start}>開始</button>
      <button onClick={stop}>停止</button>
      <button onClick={reset}>リセット</button>
    </div>
  );
}

一度作ったタイマー機能を、いろいろなコンポーネントで使い回せます。 同じ機能を何度も書く必要がありません。

3. テストが簡単になる

Hooksはテストしやすい構造になっています。

// カスタムHookのテスト
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';

describe('useCounter', () => {
  it('初期値を正しく設定する', () => {
    const { result } = renderHook(() => useCounter(5));
    expect(result.current.count).toBe(5);
  });

  it('incrementが正しく動作する', () => {
    const { result } = renderHook(() => useCounter(0));
    
    act(() => {
      result.current.increment();
    });
    
    expect(result.current.count).toBe(1);
  });

  it('decrementが正しく動作する', () => {
    const { result } = renderHook(() => useCounter(5));
    
    act(() => {
      result.current.decrement();
    });
    
    expect(result.current.count).toBe(4);
  });
});

機能ごとに独立してテストできるので、バグを見つけやすくなります。

4. アプリのサイズが小さくなる

関数コンポーネントの方が、作られるファイルサイズが小さくなります。

// クラスコンポーネント(約2KB)
class ClassComponent extends Component {
  render() {
    return <div>Hello World</div>;
  }
}

// 関数コンポーネント(約0.5KB)
function FunctionComponent() {
  return <div>Hello World</div>;
}

アプリの読み込み速度が向上して、ユーザー体験が良くなります。

5. 最新機能をいち早く使える

React公式チームは、新機能をHooks中心で開発しています。

// React 18の新機能例
import { useTransition, useDeferredValue } from 'react';

function SearchComponent() {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();
  const deferredQuery = useDeferredValue(query);

  const handleSearch = (value) => {
    startTransition(() => {
      setQuery(value);
    });
  };

  return (
    <div>
      <input
        type="text"
        onChange={(e) => handleSearch(e.target.value)}
        placeholder="検索..."
      />
      {isPending && <p>検索中...</p>}
      <SearchResults query={deferredQuery} />
    </div>
  );
}

Hooksを使っていると、最新の便利機能をすぐに活用できます。

実際に移行してみよう

では、実際のコードをクラスコンポーネントからHooksに移行してみましょう。 段階的に進めることで、安全に移行できます。

フォームコンポーネントの移行例

移行前(クラスコンポーネント)

class ContactForm extends Component {
  constructor(props) {
    super(props);
    this.state = {
      name: '',
      email: '',
      message: '',
      errors: {},
      isSubmitting: false,
      submitStatus: null
    };
  }

  handleChange = (field) => (e) => {
    this.setState({
      [field]: e.target.value,
      errors: { ...this.state.errors, [field]: '' }
    });
  };

  validateForm = () => {
    const errors = {};
    const { name, email, message } = this.state;

    if (!name.trim()) errors.name = '名前は必須です';
    if (!email.trim()) errors.email = 'メールアドレスは必須です';
    if (!message.trim()) errors.message = 'メッセージは必須です';

    return errors;
  };

  handleSubmit = async (e) => {
    e.preventDefault();
    
    const errors = this.validateForm();
    if (Object.keys(errors).length > 0) {
      this.setState({ errors });
      return;
    }

    this.setState({ isSubmitting: true, submitStatus: null });

    try {
      const response = await fetch('/api/contact', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          name: this.state.name,
          email: this.state.email,
          message: this.state.message
        })
      });

      if (response.ok) {
        this.setState({
          name: '',
          email: '',
          message: '',
          submitStatus: 'success',
          isSubmitting: false
        });
      } else {
        this.setState({
          submitStatus: 'error',
          isSubmitting: false
        });
      }
    } catch (error) {
      this.setState({
        submitStatus: 'error',
        isSubmitting: false
      });
    }
  };

  render() {
    const { name, email, message, errors, isSubmitting, submitStatus } = this.state;

    return (
      <form onSubmit={this.handleSubmit}>
        <div>
          <input
            type="text"
            value={name}
            onChange={this.handleChange('name')}
            placeholder="名前"
          />
          {errors.name && <p>{errors.name}</p>}
        </div>
        
        <div>
          <input
            type="email"
            value={email}
            onChange={this.handleChange('email')}
            placeholder="メールアドレス"
          />
          {errors.email && <p>{errors.email}</p>}
        </div>
        
        <div>
          <textarea
            value={message}
            onChange={this.handleChange('message')}
            placeholder="メッセージ"
          />
          {errors.message && <p>{errors.message}</p>}
        </div>
        
        <button type="submit" disabled={isSubmitting}>
          {isSubmitting ? '送信中...' : '送信'}
        </button>
        
        {submitStatus === 'success' && <p>送信が完了しました</p>}
        {submitStatus === 'error' && <p>送信に失敗しました</p>}
      </form>
    );
  }
}

移行後(Hooks)

まず、カスタムHooksを作って機能を分割します。

import React, { useState } from 'react';

// カスタムHook: フォームの状態管理
function useForm(initialState, validate) {
  const [values, setValues] = useState(initialState);
  const [errors, setErrors] = useState({});

  const handleChange = (field) => (e) => {
    setValues(prev => ({ ...prev, [field]: e.target.value }));
    setErrors(prev => ({ ...prev, [field]: '' }));
  };

  const validateForm = () => {
    const validationErrors = validate(values);
    setErrors(validationErrors);
    return Object.keys(validationErrors).length === 0;
  };

  const resetForm = () => {
    setValues(initialState);
    setErrors({});
  };

  return {
    values,
    errors,
    handleChange,
    validateForm,
    resetForm
  };
}

// カスタムHook: API送信
function useSubmit(endpoint) {
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [submitStatus, setSubmitStatus] = useState(null);

  const submit = async (data) => {
    setIsSubmitting(true);
    setSubmitStatus(null);

    try {
      const response = await fetch(endpoint, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data)
      });

      if (response.ok) {
        setSubmitStatus('success');
      } else {
        setSubmitStatus('error');
      }
    } catch (error) {
      setSubmitStatus('error');
    } finally {
      setIsSubmitting(false);
    }
  };

  return { isSubmitting, submitStatus, submit };
}

// メインコンポーネント
function ContactForm() {
  const initialState = { name: '', email: '', message: '' };
  
  const validate = (values) => {
    const errors = {};
    if (!values.name.trim()) errors.name = '名前は必須です';
    if (!values.email.trim()) errors.email = 'メールアドレスは必須です';
    if (!values.message.trim()) errors.message = 'メッセージは必須です';
    return errors;
  };

  const { values, errors, handleChange, validateForm, resetForm } = useForm(initialState, validate);
  const { isSubmitting, submitStatus, submit } = useSubmit('/api/contact');

  const handleSubmit = async (e) => {
    e.preventDefault();
    
    if (!validateForm()) return;
    
    await submit(values);
    
    if (submitStatus === 'success') {
      resetForm();
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          type="text"
          value={values.name}
          onChange={handleChange('name')}
          placeholder="名前"
        />
        {errors.name && <p>{errors.name}</p>}
      </div>
      
      <div>
        <input
          type="email"
          value={values.email}
          onChange={handleChange('email')}
          placeholder="メールアドレス"
        />
        {errors.email && <p>{errors.email}</p>}
      </div>
      
      <div>
        <textarea
          value={values.message}
          onChange={handleChange('message')}
          placeholder="メッセージ"
        />
        {errors.message && <p>{errors.message}</p>}
      </div>
      
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? '送信中...' : '送信'}
      </button>
      
      {submitStatus === 'success' && <p>送信が完了しました</p>}
      {submitStatus === 'error' && <p>送信に失敗しました</p>}
    </form>
  );
}

移行後のコードは、以下の点で改善されています。

  • 機能が分離されている: フォーム管理とAPI送信が別々のカスタムHookになっている
  • 再利用しやすい: 他のフォームでも同じカスタムHookが使える
  • テストしやすい: 各機能を独立してテストできる
  • 読みやすい: メインコンポーネントがシンプルになった

データ取得コンポーネントの移行例

次に、APIからデータを取得するコンポーネントを移行してみましょう。

移行前(クラスコンポーネント)

class UserList extends Component {
  constructor(props) {
    super(props);
    this.state = {
      users: [],
      loading: true,
      error: null,
      searchTerm: '',
      sortBy: 'name'
    };
  }

  componentDidMount() {
    this.fetchUsers();
  }

  fetchUsers = async () => {
    try {
      this.setState({ loading: true });
      const response = await fetch('/api/users');
      const users = await response.json();
      this.setState({ users, loading: false });
    } catch (error) {
      this.setState({ error: error.message, loading: false });
    }
  };

  handleSearch = (e) => {
    this.setState({ searchTerm: e.target.value });
  };

  handleSort = (e) => {
    this.setState({ sortBy: e.target.value });
  };

  getFilteredUsers = () => {
    const { users, searchTerm, sortBy } = this.state;
    
    return users
      .filter(user =>
        user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
        user.email.toLowerCase().includes(searchTerm.toLowerCase())
      )
      .sort((a, b) => {
        if (sortBy === 'name') return a.name.localeCompare(b.name);
        if (sortBy === 'email') return a.email.localeCompare(b.email);
        return 0;
      });
  };

  render() {
    const { loading, error, searchTerm, sortBy } = this.state;
    const filteredUsers = this.getFilteredUsers();

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

    return (
      <div>
        <input
          type="text"
          value={searchTerm}
          onChange={this.handleSearch}
          placeholder="ユーザー検索"
        />
        <select value={sortBy} onChange={this.handleSort}>
          <option value="name">名前順</option>
          <option value="email">メール順</option>
        </select>
        
        <ul>
          {filteredUsers.map(user => (
            <li key={user.id}>
              {user.name} - {user.email}
            </li>
          ))}
        </ul>
      </div>
    );
  }
}

移行後(Hooks)

import React, { useState, useEffect, useMemo } from 'react';

// カスタムHook: データ取得
function useApi(endpoint) {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        const response = await fetch(endpoint);
        if (!response.ok) throw new Error('データの取得に失敗しました');
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [endpoint]);

  return { data, loading, error };
}

// カスタムHook: 検索とソート
function useFilterAndSort(data, searchTerm, sortBy) {
  return useMemo(() => {
    return data
      .filter(item =>
        item.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
        item.email.toLowerCase().includes(searchTerm.toLowerCase())
      )
      .sort((a, b) => {
        if (sortBy === 'name') return a.name.localeCompare(b.name);
        if (sortBy === 'email') return a.email.localeCompare(b.email);
        return 0;
      });
  }, [data, searchTerm, sortBy]);
}

// メインコンポーネント
function UserList() {
  const [searchTerm, setSearchTerm] = useState('');
  const [sortBy, setSortBy] = useState('name');
  
  const { data: users, loading, error } = useApi('/api/users');
  const filteredUsers = useFilterAndSort(users, searchTerm, sortBy);

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

  return (
    <div>
      <input
        type="text"
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="ユーザー検索"
      />
      <select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
        <option value="name">名前順</option>
        <option value="email">メール順</option>
      </select>
      
      <ul>
        {filteredUsers.map(user => (
          <li key={user.id}>
            {user.name} - {user.email}
          </li>
        ))}
      </ul>
    </div>
  );
}

この移行によって以下の改善が得られました。

  • データ取得ロジックが再利用可能になった
  • フィルタリングとソートが最適化されたuseMemo使用)
  • コードが読みやすくなった
  • 機能ごとにテストしやすくなった

移行時に気をつけるポイント

Hooksに移行する時に、よくある落とし穴とその対策をご紹介します。

1. 段階的に移行しよう

一度に全部変更するのではなく、少しずつ進めましょう。

// 段階1: 新しいコンポーネントはHooksで作成
function NewComponent() {
  const [state, setState] = useState();
  return <div>{state}</div>;
}

// 段階2: 既存のシンプルなコンポーネントを移行
function SimpleComponent() {
  // クラスコンポーネントから移行
}

// 段階3: 複雑なコンポーネントを移行
function ComplexComponent() {
  // 複数のカスタムHooksを使用
}

小さなコンポーネントから始めて、慣れてきたら大きなものに挑戦しましょう。

2. useEffectの依存配列を正しく指定

これは初心者がよくつまずくポイントです。

// ❌ 間違った例:依存配列の指定ミス
function BadComponent({ userId }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, []); // userIdが変わっても再実行されない!
  
  return <div>{user?.name}</div>;
}

// ✅ 正しい例:適切な依存配列
function GoodComponent({ userId }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]); // userIdが変わったら再実行される
  
  return <div>{user?.name}</div>;
}

useEffectで使っている変数は、必ず依存配列に含めましょう。

3. 無限ループを避ける

useEffectで状態を更新する時は注意が必要です。

// ❌ 無限ループが発生する例
function BadComponent() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    setCount(count + 1); // 毎回実行される!
  }); // 依存配列がないと毎回実行される
  
  return <div>{count}</div>;
}

// ✅ 正しい例:適切な依存配列で回避
function GoodComponent() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    // 初回のみ実行したい場合
    setCount(1);
  }, []); // 空の依存配列で初回のみ
  
  return <div>{count}</div>;
}

依存配列を正しく指定することで、無限ループを防げます。

4. クリーンアップ処理を忘れずに

タイマーやイベントリスナーは、コンポーネントが削除される時に停止しましょう。

// タイマーの適切なクリーンアップ
function TimerComponent() {
  const [seconds, setSeconds] = useState(0);
  
  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);
    
    // クリーンアップ関数でタイマーを停止
    return () => clearInterval(interval);
  }, []);
  
  return <div>{seconds}秒</div>;
}

// イベントリスナーの適切なクリーンアップ
function EventListenerComponent() {
  useEffect(() => {
    const handleResize = () => {
      console.log('ウィンドウサイズが変更されました');
    };
    
    window.addEventListener('resize', handleResize);
    
    // クリーンアップ関数でイベントリスナーを削除
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);
  
  return <div>ウィンドウサイズの変更を監視中</div>;
}

クリーンアップ処理を忘れると、メモリリークの原因になることがあります。

まとめ:Hooksで快適なReact開発を!

React Hooksについて、基本から実践的な使い方まで詳しく解説しました。

React Hooksの魅力

  1. コードがスッキリして読みやすい
  2. 機能を簡単に再利用できる
  3. テストが簡単になる
  4. アプリのサイズが小さくなる
  5. 最新機能をいち早く使える

移行時のポイント

  • 段階的に進める: 小さなコンポーネントから始める
  • 依存配列に注意: useEffectで使う変数は必ず含める
  • 無限ループを避ける: 適切な依存配列で制御
  • クリーンアップ処理: タイマーやイベントリスナーは忘れずに停止

今日から始められること

  1. 新しいコンポーネントはHooksで作る
  2. 小さなクラスコンポーネントを移行してみる
  3. カスタムHooksを作って機能を再利用する
  4. React Developer Toolsでstateの変化を確認する

最初は慣れないかもしれませんが、Hooksを使い始めると「もうクラスコンポーネントには戻れない!」と感じるはずです。 ぜひ実際のプロジェクトでHooksを試してみてください。

きっと、より効率的で楽しいReact開発ができるようになりますよ!

関連記事