Reactの基礎 - Hooksの基礎

提供: MochiuWiki : SUSE, EC, PCB

概要

React Hooksは、React 16.8 (2019年2月リリース) で導入された機能である。
Hooksの登場以前は、ステート管理やライフサイクル処理にはクラスコンポーネントが必要であったが、Hooksの導入によって関数コンポーネントでもこれらの処理が実現できるようになった。

クラスコンポーネントには、以下に示すような問題点が存在していた。

  • ロジックの再利用が困難であり、コンポーネント間で状態を持つロジックを共有するためには、Higher-Order Components (HOC)やRender Props等の複雑なパターンが必要であった。
  • ContextプロバイダやHOCを多数組み合わせることで深くネストした「Wrapper Hell」が発生しやすく、コードの可読性が低下するという問題もあった。
  • ライフサイクルメソッドに無関係なロジックが混在しがちになり、コンポーネントが肥大化する傾向があった。


Hooksはこれらの問題を解決するために設計された。
カスタムHookを使用することにより、状態を持つロジックをコンポーネント間で簡単に再利用できるようになった。
また、useEffect によってライフサイクルのロジックを関心事ごとにまとめて記述できるようになり、処理の見通しが大幅に向上した。

現在のReact開発では、関数コンポーネントとHooksを組み合わせた記述スタイルが標準となっている。
新規プロジェクトではクラスコンポーネントを使用する必要はなく、関数コンポーネントとHooksのみで、かつてクラスコンポーネントが担っていた全ての処理を定義できる。


Hooksとは

Hooksは、関数コンポーネントに対してステート管理やライフサイクル処理、コンテキストへのアクセス等の機能を追加するための関数群である。
Hook の名前は慣例として use で始まる。

基本的な使い方

useStateuseEffect を使用した基本的な関数コンポーネントの例を以下に示す。

以下の例では、useState でステートを定義し、useEffect でカウントの変化に応じてドキュメントのタイトルを更新している。
クラスコンポーネントで必要であった this の参照やバインディング処理は一切不要である。

 import { useState, useEffect } from 'react';
 
 function Counter() {
    const [count, setCount] = useState<number>(0);
 
    useEffect(() => {
       document.title = `カウント: ${count}`;
    }, [count]);
 
    return (
       <div>
          <p>カウント: {count}</p>
          <button onClick={() => setCount(count + 1)}>増加</button>
       </div>
    );
 }
 
 export default Counter;



Hooksの基本ルール

Hooksを正しく使用するためには、Reactが定める2つの基本ルールを厳守する必要がある。

これらのルールは、ReactがHookの呼び出し順序に基づいて内部状態を管理していることに起因する。

トップレベルでのみ呼び出す

Hooksは、必ずコンポーネント関数 または カスタムHookのトップレベルで呼び出さなければならない。
条件分岐 (if / else)、ループ (for / while)、ネストされた関数の内部では呼び出してはならない。

Reactは、レンダリングのたびにHookが常に同じ順序で呼び出されることを前提として、各Hookに対応する内部状態を管理している。
条件分岐やループの中でHookを呼び出すと、ある条件下ではHookがスキップされ、呼び出し順序が変化してしまう。

これにより、Reactが正しい内部状態をHookに対応付けられなくなり、バグが発生する。

  • 正しい例 (トップレベルで呼び出す)
     import { useState, useEffect } from 'react';
     
     interface UserProfileProps {
        userId: string;
        showDetails: boolean;
     }
     
     function UserProfile({ userId, showDetails }: UserProfileProps) {
        // 正しい : トップレベルで常に呼び出される
        const [user, setUser] = useState<string | null>(null);
        const [isLoading, setIsLoading] = useState<boolean>(true);
     
        useEffect(() => {
           // 条件分岐はuseEffectの中に入れる
           if (!showDetails) return;
           setIsLoading(false);
        }, [userId, showDetails]);
     
        if (!showDetails) {
           return <p>詳細を非表示</p>;
        }
     
        return <p>ユーザ: {user}</p>;
     }
    

  • 誤りの例 (条件分岐の中で呼び出す)
     import { useState, useEffect } from 'react';
     
     interface UserProfileProps {
        userId: string;
        showDetails: boolean;
     }
     
     function UserProfile({ userId, showDetails }: UserProfileProps) {
        const [user, setUser] = useState<string | null>(null);
     
        // 誤り : 条件分岐の中でHookを呼び出してはならない
        if (showDetails) {
           useEffect(() => {
              setUser('田中 太郎');
           }, [userId]);
        }
     
        return <p>ユーザ: {user}</p>;
     }
    


関数コンポーネント または カスタムHook内でのみ使用

Hooksは、Reactの関数コンポーネントまたはカスタムHookの内部でのみ呼び出せる。
通常のTypeScript / JavaScript関数、クラスコンポーネントのメソッド、イベントハンドラの外部から直接呼び出すことはできない。

カスタムHookとは、use で始まる名前を持つ関数であり、その内部で他のHookを呼び出すことができる。

カスタムHookを使用することにより、ステートを持つロジックを複数のコンポーネント間で再利用できる。

 import { useState } from 'react';
 
 // 正しい : カスタムHookはuseで始まる関数として定義する
 function useCounter(initialValue: number) {
    const [count, setCount] = useState<number>(initialValue);
    const increment = () => setCount(c => c + 1);
    const decrement = () => setCount(c => c - 1);
    const reset = () => setCount(initialValue);
    return { count, increment, decrement, reset };
 }
 
 // 正しい : 関数コンポーネント内で使用する
 function CounterDisplay() {
    const { count, increment, decrement, reset } = useCounter(0);
 
    return (
       <div>
          <p>カウント: {count}</p>
          <button onClick={increment}>増加</button>
          <button onClick={decrement}>減少</button>
          <button onClick={reset}>リセット</button>
       </div>
    );
 }


eslint-plugin-react-hooks

Hooksのルール違反を自動検出するために、eslint-plugin-react-hooks の使用が公式に推奨されている。

このプラグインには、以下に示す2つのルールが含まれている。

  • rules-of-hooks
    Hooksの2つの基本ルール (トップレベルでの呼び出し、関数コンポーネントまたはカスタムHook内での使用) を強制するルールである。
    違反した場合はエラー (error) として報告される。
  • exhaustive-deps
    useEffectuseCallbackuseMemo 等の依存配列に、必要な全ての依存値が含まれているかを検証するルールである。
    依存配列の記述漏れを警告 (warn) として報告する。


推奨設定を以下に示す。

 {
    "plugins": ["react-hooks"],
    "rules": {
       "react-hooks/rules-of-hooks": "error",
       "react-hooks/exhaustive-deps": "warn"
    }
 }


Create React App および Next.jsでは、このプラグインがデフォルトで有効になっている。
Vite等の他のビルドツールを使用する場合は、手動でインストールおよび設定する必要がある。

npm install --save-dev eslint-plugin-react-hooks



Hooks一覧と分類

下表に、Reactが提供する全ての組み込みHookを分類したものを示す。

React 組み込みHooks一覧
分類 Hook名 説明 導入バージョン
状態管理系 useState コンポーネントにローカルなステートを追加する。
値とセッター関数のペアを返す。
React 16.8
状態管理系 useReducer 複雑なステート更新ロジックをreducer関数で管理する。
useState の代替として使用する。
React 16.8
副作用系 useEffect レンダリング後に副作用処理 (データ取得、サブスクリプション、DOM操作等) を実行する。 React 16.8
副作用系 useLayoutEffect DOMの変更後、Webブラウザの描画前に同期的に副作用を実行する。
レイアウト計算が必要な場合に使用する。
React 16.8
副作用系 useInsertionEffect CSS-in-JSライブラリ向けのHook
DOMへの変更前に同期的に実行される。
React 18
参照系 useRef レンダリングをトリガーしないミュータブルな参照値を保持する。
DOMノードへのアクセスにも使用する。
React 16.8
参照系 useImperativeHandle forwardRef と組み合わせて、親コンポーネントに公開するrefの値をカスタマイズする。 React 16.8
コンテキスト系 useContext React Context の値を読み取る。
プロバイダの最も近い祖先から値を取得する。
React 16.8
パフォーマンス系 useMemo 計算コストの高い値をメモ化する。
依存値が変化した時のみ再計算する。
React 16.8
パフォーマンス系 useCallback コールバック関数をメモ化する。
依存値が変化した時のみ新しい関数を生成する。
React 16.8
パフォーマンス系 useTransition ステート更新を「緊急でない」トランジションとしてマークし、UIの応答性を維持する。 React 18
パフォーマンス系 useDeferredValue 値の更新を遅延させ、より緊急な更新を優先してレンダリングする。 React 18
ID系 useId サーバとクライアント間で一貫したユニークIDを生成する。
アクセシビリティ属性の紐付けに使用する。
React 18
外部ストア系 useSyncExternalStore Reactの外部にあるデータストア (Redux 等) をサブスクライブするためのHook React 18
デバッグ系 useDebugValue カスタムHookに対して、React DevToolsでのラベルを設定する。
デバッグ用途に使用する。
React 16.8
React 19新規 use PromiseやContextの値をレンダリング中に直接読み取る。
条件分岐内でも使用できる。
React 19
React 19新規 useActionState Server Actionsの実行状態と結果を管理する。
フォーム送信に対応する。
React 19
React 19新規 useFormStatus 親フォームの送信状態 (送信中かどうか等) を読み取る。 React 19
React 19新規 useOptimistic 非同期処理の完了前に楽観的にUIを更新し、応答性を向上させる。 React 19



React 19での変更点

React 19では、新しいHookの追加と既存のAPIの簡略化が行われた。

新しいHooks

React 19で追加された新しいHookを以下に示す。

  • use
    Promise や Contextの値をレンダリング中に直接読み取ることができるHookである。
    従来のHookとは異なり、条件分岐やループの中でも呼び出すことができる。
    Promiseを渡した場合、Suspenseと組み合わせてデータが解決されるまでコンポーネントのレンダリングを中断する。
     import { use, Suspense } from 'react';
     
     interface User {
        id: number;
        name: string;
     }
     
     function UserName({ userPromise }: { userPromise: Promise<User> }) {
        // useはPromiseが解決されるまでSuspenseをトリガーする
        const user = use(userPromise);
        return <p>ユーザ名: {user.name}</p>;
     }
     
     function App() {
        const userPromise = fetch('/api/user').then(res => res.json() as Promise<User>);
     
        return (
           <Suspense fallback={<p>読み込み中...</p>}>
              <UserName userPromise={userPromise} />
           </Suspense>
        );
     }
    

  • useActionState
    Server Actionsの実行状態と結果を管理するHookである。
    フォームの送信処理で使用し、アクションの実行状態 (pending)、前回の実行結果、アクション関数を返す。
     'use client';
     import { useActionState } from 'react';
     
     async function submitForm(prevState: string, formData: FormData): Promise<string> {
        const name = formData.get('name') as string;
        return `送信完了: ${name}`;
     }
     
     function ContactForm() {
        const [message, formAction, isPending] = useActionState(submitForm, '');
     
        return (
           <form action={formAction}>
              <input name="name" type="text" placeholder="名前" />
              <button type="submit" disabled={isPending}>
                 {isPending ? '送信中...' : '送信'}
              </button>
              {message && <p>{message}</p>}
           </form>
        );
     }
    

  • useFormStatus
    親フォームの送信状態を読み取るHookである。
    フォームの子コンポーネント内で使用し、送信中かどうかを示す pending フラグ等を取得できる。
     'use client';
     import { useFormStatus } from 'react-dom';
     
     function SubmitButton() {
        // 親フォームの送信状態を読み取る
        const { pending } = useFormStatus();
     
        return (
           <button type="submit" disabled={pending}>
              {pending ? '送信中...' : '送信'}
           </button>
        );
     }
     
     function ContactForm() {
        async function handleSubmit(formData: FormData) {
           'use server';
           // Server Actionの処理
        }
     
        return (
           <form action={handleSubmit}>
              <input name="email" type="email" />
              <SubmitButton />
           </form>
        );
     }
    

  • useOptimistic
    非同期処理の完了を待たずに、UIを楽観的に更新するHookである。
    ユーザのアクションに対して即座にUIを更新し、処理が完了したら実際の結果を反映する。
    処理が失敗した場合は自動的に元の状態に戻る。
     'use client';
     import { useOptimistic, useState } from 'react';
     
     interface Message {
        id: number;
        text: string;
        sending?: boolean;
     }
     
     async function sendMessageToServer(text: string): Promise<Message> {
        // サーバへの送信処理 (省略)
        return { id: Date.now(), text };
     }
     
     function MessageList() {
        const [messages, setMessages] = useState<Message[]>([]);
        const [optimisticMessages, addOptimisticMessage] = useOptimistic<Message[], Message>(
           messages,
           (state, newMessage) => [...state, newMessage]
        );
     
        async function handleSubmit(formData: FormData) {
           const text = formData.get('message') as string;
           const tempMessage: Message = { id: -1, text, sending: true };
     
           // 楽観的に即座にUIを更新する
           addOptimisticMessage(tempMessage);
     
           // 実際のサーバ処理
           const savedMessage = await sendMessageToServer(text);
           setMessages(prev => [...prev, savedMessage]);
        }
     
        return (
           <div>
              <ul>
                 {optimisticMessages.map(msg => (
                    <li key={msg.id} style={{ opacity: msg.sending ? 0.5 : 1 }}>
                       {msg.text} {msg.sending && '(送信中...)'}
                    </li>
                 ))}
              </ul>
              <form action={handleSubmit}>
                 <input name="message" type="text" />
                 <button type="submit">送信</button>
              </form>
           </div>
        );
     }
    


ref as prop

React 19では、ref を通常のpropとして関数コンポーネントに渡せるようになった。
これにより、React 18以前で必要であった forwardRef のラッパーが不要になった。

  • React 18以前 (forwardRef が必要)
     import { forwardRef, useRef } from 'react';
     
     // React 18以前 : forwardRefでラップする必要があった
     const TextInput = forwardRef<HTMLInputElement, { placeholder?: string }>(
        ({ placeholder }, ref) => {
           return <input ref={ref} placeholder={placeholder} />;
        }
     );
     TextInput.displayName = 'TextInput';
     
     function Form() {
        const inputRef = useRef<HTMLInputElement>(null);
     
        const focusInput = () => {
           inputRef.current?.focus();
        };
     
        return (
           <div>
              <TextInput ref={inputRef} placeholder="入力してください" />
              <button onClick={focusInput}>フォーカス</button>
           </div>
        );
     }
    

  • React 19 (forwardRef 不要)
     import { useRef, Ref } from 'react';
     
     interface TextInputProps {
        placeholder?: string;
        ref?: Ref<HTMLInputElement>;
     }
     
     // React 19 : refを通常のpropとして受け取れる
     function TextInput({ placeholder, ref }: TextInputProps) {
        return <input ref={ref} placeholder={placeholder} />;
     }
     
     function Form() {
        const inputRef = useRef<HTMLInputElement>(null);
     
        const focusInput = () => {
           inputRef.current?.focus();
        };
     
        return (
           <div>
              <TextInput ref={inputRef} placeholder="入力してください" />
              <button onClick={focusInput}>フォーカス</button>
           </div>
        );
     }
    


React 18以前に forwardRef を使用して作成した既存のコンポーネントは、React 19でも引き続き動作する。
ただし、forwardRef は将来的に非推奨となる予定であるため、新規に作成するコンポーネントでは ref as prop の形式を使用することを推奨する。


関連情報