React Hooks完全ガイド(useState, useEffect, useContext など)

Reactの世界では、Hooksの導入により関数コンポーネントでより強力な機能が使えるようになりました。この記事では、主要なReact Hooksについて詳しく説明します。

目次

  1. Hooksとは何か
  2. useState – 状態管理の基本
  3. useEffect – 副作用の処理
  4. useContext – コンテキストの活用
  5. useReducer – 複雑な状態管理
  6. useRef – DOMへの参照と値の保持
  7. useMemo – 計算結果のメモ化
  8. useCallback – 関数のメモ化
  9. カスタムHook – 独自のロジックの再利用
  10. Hooksのベストプラクティス

1. Hooksとは何か

Hooksは、React 16.8で導入された機能で、クラスコンポーネントを書かなくても状態(state)やライフサイクルメソッドなどの機能を関数コンポーネントで利用できるようにします。

// クラスコンポーネント
class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }
  
  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

// 関数コンポーネント + Hooks
function Example() {
  const [count, setCount] = React.useState(0);
  
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

2. useState – 状態管理の基本

useStateは最も基本的なHookで、関数コンポーネント内で状態を管理するために使います。

基本的な使い方

const [state, setState] = useState(initialState);

useStateは現在の状態値と、その値を更新する関数を返します。初期状態は最初のレンダリング時にのみ使用されます。

例: カウンターコンポーネント

function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>カウント: {count}</p>
      <button onClick={() => setCount(count + 1)}>増加</button>
      <button onClick={() => setCount(count - 1)}>減少</button>
    </div>
  );
}

関数型の更新

前の状態に基づいて状態を更新する場合は、関数を渡すことができます:

setCount(prevCount => prevCount + 1);

複数の状態変数

複数の状態を管理する場合は、複数のuseStateを使用します:

function UserForm() {
  const [name, setName] = useState('');
  const [age, setAge] = useState(0);
  const [email, setEmail] = useState('');
  
  // ...
}

3. useEffect – 副作用の処理

useEffectは、データの取得、DOM操作、購読など、関数コンポーネント内で副作用を実行するためのHookです。

基本的な使い方

useEffect(() => {
  // 副作用のコード
  
  // クリーンアップ関数(オプション)
  return () => {
    // コンポーネントのアンマウント時やeffectの再実行前に実行
  };
}, [依存配列]); // 依存配列が変更されたときのみ実行

例: データフェッチング

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    const fetchUser = async () => {
      setLoading(true);
      try {
        const response = await fetch(`https://api.example.com/users/${userId}`);
        const data = await response.json();
        setUser(data);
      } catch (error) {
        console.error('ユーザー情報の取得に失敗しました', error);
      } finally {
        setLoading(false);
      }
    };
    
    fetchUser();
  }, [userId]); // userIdが変わるたびに再取得
  
  if (loading) return <p>読み込み中...</p>;
  if (!user) return <p>ユーザーが見つかりません</p>;
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
    </div>
  );
}

依存配列の重要性

  • 空の配列 []: コンポーネントのマウント時に1回だけ実行
  • 依存配列なし: レンダリングごとに実行
  • 依存値を含む配列 [value1, value2]: 依存値が変更されたときに実行

クリーンアップ

イベントリスナーやタイマーなどのリソースは、クリーンアップ関数を使って解放します:

useEffect(() => {
  const timer = setInterval(() => {
    console.log('タイマー実行中');
  }, 1000);
  
  return () => {
    clearInterval(timer); // クリーンアップ
  };
}, []);

4. useContext – コンテキストの活用

useContextは、Reactのコンテキストシステムを使って、コンポーネントツリー全体でデータを共有するためのHookです。

基本的な使い方

const value = useContext(MyContext);

例: テーマの共有

// テーマコンテキストを作成
const ThemeContext = React.createContext('light');

function App() {
  const [theme, setTheme] = useState('light');
  
  return (
    <ThemeContext.Provider value={theme}>
      <div>
        <Header />
        <Main />
        <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
          テーマ切替
        </button>
      </div>
    </ThemeContext.Provider>
  );
}

function Header() {
  const theme = useContext(ThemeContext);
  return (
    <header className={`header-${theme}`}>
      <h1>My App</h1>
    </header>
  );
}

function Main() {
  const theme = useContext(ThemeContext);
  return (
    <main className={`main-${theme}`}>
      <p>コンテンツ</p>
    </main>
  );
}

5. useReducer – 複雑な状態管理

useReducerは、複雑な状態ロジックを管理するためのuseStateの代替手段です。Reduxに似た状態管理パターンを提供します。

基本的な使い方

const [state, dispatch] = useReducer(reducer, initialState);

例: TODOリスト

function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, { id: Date.now(), text: action.payload, completed: false }];
    case 'TOGGLE_TODO':
      return state.map(todo => 
        todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo
      );
    case 'DELETE_TODO':
      return state.filter(todo => todo.id !== action.payload);
    default:
      return state;
  }
}

function TodoApp() {
  const [todos, dispatch] = useReducer(todoReducer, []);
  const [text, setText] = useState('');
  
  const handleSubmit = e => {
    e.preventDefault();
    if (!text.trim()) return;
    dispatch({ type: 'ADD_TODO', payload: text });
    setText('');
  };
  
  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input 
          value={text} 
          onChange={e => setText(e.target.value)} 
          placeholder="TODOを入力" 
        />
        <button type="submit">追加</button>
      </form>
      <ul>
        {todos.map(todo => (
          <li 
            key={todo.id}
            style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
          >
            <span onClick={() => dispatch({ type: 'TOGGLE_TODO', payload: todo.id })}>
              {todo.text}
            </span>
            <button onClick={() => dispatch({ type: 'DELETE_TODO', payload: todo.id })}>
              削除
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

6. useRef – DOMへの参照と値の保持

useRefは、DOMノードへの参照を保持したり、再レンダリング間で値を保持するために使用します。

基本的な使い方

const refContainer = useRef(initialValue);

例: DOM要素へのアクセス

function TextInputWithFocusButton() {
  const inputRef = useRef(null);
  
  const focusInput = () => {
    inputRef.current.focus();
  };
  
  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>
        フォーカス
      </button>
    </div>
  );
}

例: レンダリング間で値を保持

function Timer() {
  const [count, setCount] = useState(0);
  const intervalRef = useRef(null);
  
  const startTimer = () => {
    if (intervalRef.current !== null) return;
    intervalRef.current = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
  };
  
  const stopTimer = () => {
    clearInterval(intervalRef.current);
    intervalRef.current = null;
  };
  
  useEffect(() => {
    // コンポーネントのアンマウント時にタイマーをクリア
    return () => clearInterval(intervalRef.current);
  }, []);
  
  return (
    <div>
      <p>カウント: {count}</p>
      <button onClick={startTimer}>開始</button>
      <button onClick={stopTimer}>停止</button>
    </div>
  );
}

7. useMemo – 計算結果のメモ化

useMemoは、パフォーマンス最適化のために計算結果をメモ化します。依存配列の値が変更された場合にのみ再計算されます。

基本的な使い方

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

例: フィルタリングと並べ替え

function ProductList({ products, filterText, sortBy }) {
  const filteredAndSortedProducts = useMemo(() => {
    console.log('製品リストを再計算');
    
    // フィルタリング
    const filtered = products.filter(product => 
      product.name.toLowerCase().includes(filterText.toLowerCase())
    );
    
    // 並べ替え
    return [...filtered].sort((a, b) => {
      if (sortBy === 'name') {
        return a.name.localeCompare(b.name);
      } else if (sortBy === 'price') {
        return a.price - b.price;
      }
      return 0;
    });
  }, [products, filterText, sortBy]); // 依存配列
  
  return (
    <ul>
      {filteredAndSortedProducts.map(product => (
        <li key={product.id}>
          {product.name} - ¥{product.price}
        </li>
      ))}
    </ul>
  );
}

8. useCallback – 関数のメモ化

useCallbackは、コールバック関数をメモ化します。子コンポーネントに関数を渡す場合や、依存リストに関数を持つ場合に特に有用です。

基本的な使い方

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

例: 子コンポーネントの最適化

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');
  
  // textが変わらない限り、関数は再作成されない
  const handleClick = useCallback(() => {
    console.log('ボタンがクリックされました', text);
  }, [text]);
  
  return (
    <div>
      <ChildComponent onClick={handleClick} />
      <button onClick={() => setCount(count + 1)}>
        カウント: {count}
      </button>
      <input
        value={text}
        onChange={e => setText(e.target.value)}
      />
    </div>
  );
}

// React.memoを使って、propsが変わらない限り再レンダリングしない
const ChildComponent = React.memo(({ onClick }) => {
  console.log('子コンポーネントがレンダリングされました');
  return <button onClick={onClick}>クリック</button>;
});

9. カスタムHook – 独自のロジックの再利用

カスタムHookを作成することで、コンポーネント間でロジックを再利用できます。

例: フォーム処理のカスタムHook

// useFormというカスタムHookを作成
function useForm(initialValues) {
  const [values, setValues] = useState(initialValues);
  
  const handleChange = e => {
    const { name, value } = e.target;
    setValues(prevValues => ({
      ...prevValues,
      [name]: value
    }));
  };
  
  const resetForm = () => {
    setValues(initialValues);
  };
  
  return {
    values,
    handleChange,
    resetForm
  };
}

// カスタムHookを使用
function SignupForm() {
  const { values, handleChange, resetForm } = useForm({
    username: '',
    email: '',
    password: ''
  });
  
  const handleSubmit = e => {
    e.preventDefault();
    console.log('送信データ:', values);
    // APIに送信するロジック
    resetForm();
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>ユーザー名:</label>
        <input
          type="text"
          name="username"
          value={values.username}
          onChange={handleChange}
        />
      </div>
      <div>
        <label>メール:</label>
        <input
          type="email"
          name="email"
          value={values.email}
          onChange={handleChange}
        />
      </div>
      <div>
        <label>パスワード:</label>
        <input
          type="password"
          name="password"
          value={values.password}
          onChange={handleChange}
        />
      </div>
      <button type="submit">登録</button>
    </form>
  );
}

例: APIデータの取得用カスタムHook

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    const controller = new AbortController();
    const signal = controller.signal;
    
    const fetchData = async () => {
      setLoading(true);
      try {
        const response = await fetch(url, { signal });
        if (!response.ok) {
          throw new Error(`APIエラー: ${response.status}`);
        }
        const result = await response.json();
        setData(result);
        setError(null);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err.message);
          setData(null);
        }
      } finally {
        setLoading(false);
      }
    };
    
    fetchData();
    
    return () => {
      controller.abort();
    };
  }, [url]);
  
  return { data, loading, error };
}

// 使用例
function UserList() {
  const { data, loading, error } = useFetch('https://api.example.com/users');
  
  if (loading) return <p>読み込み中...</p>;
  if (error) return <p>エラー: {error}</p>;
  
  return (
    <ul>
      {data && data.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

10. Hooksのベストプラクティス

Hooksのルール

  1. トップレベルでのみHooksを呼び出す:条件文、ループ、ネストされた関数の中でHooksを呼び出さない
  2. Reactの関数コンポーネント内またはカスタムHook内でのみHooksを呼び出す:通常のJavaScript関数では呼び出さない

依存配列の適切な管理

  • すべての依存関係を正確に列挙する
  • 依存関係を減らすために、コールバック関数内でのみ使用する値は依存配列に含めない方法を検討する
  • 必要なときにuseCallbackuseMemoを使って依存関係を安定化させる

パフォーマンス最適化

  • 必要なときにのみuseMemouseCallbackを使用する(すべてのレンダリングで使うわけではない)
  • 大きなリストのレンダリングにはReact.memoとの組み合わせを検討する
  • 状態更新が頻繁な場合はuseReducerの使用を検討する

カスタムHookの設計

  • 単一責任の原則に従う:一つのカスタムHookは一つの機能に焦点を当てる
  • 命名規則:useプレフィックスを付ける
  • 返り値を明確にする:オブジェクトまたは配列を使って複数の値を返す

まとめ

React Hooksは、関数コンポーネントに強力な機能を提供し、コードの再利用性と読みやすさを向上させます。この記事で紹介した基本的なHooksを使いこなすことで、より効率的で保守しやすいReactアプリケーションを開発できるようになります。

複雑なアプリケーションでは、これらのHooksを組み合わせて使用したり、独自のカスタムHooksを作成したりすることで、コードの管理をさらに改善できます。Reactの公式ドキュメントも参考にしながら、実際のプロジェクトでHooksを活用してみてください。

コメント

タイトルとURLをコピーしました