0
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のカスタムフック入門 — ロジックを再利用する

0
Posted at

Reactのカスタムフック入門 — ロジックを再利用する

はじめに

React入門シリーズ7回目。今回は カスタムフック

useState / useEffect / useContext と「使うだけ」のフックを学んできましたが、いよいよ「自分で作る側」に回ります。

カスタムフックは、「複数のコンポーネントで使い回したいロジックを、関数として切り出す」 ための仕組み。

Javaエンジニアへの橋渡し: 「Spring の Service 層に切り出すのと同じ発想」 — UI から離して、再利用可能なロジック単位にまとめる、それだけです。


1. なぜカスタムフックが必要か

例えば、複数の画面で「ローカルストレージから値を読んで、変わったら保存する」処理を書きたい時。

普通に書くとこんな感じになります:

function SettingsPage() {
    const [theme, setTheme] = useState(() =>
        localStorage.getItem("theme") ?? "light"
    );

    useEffect(() => {
        localStorage.setItem("theme", theme);
    }, [theme]);

    return <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>切替</button>;
}

function UserPreferencesPage() {
    const [lang, setLang] = useState(() =>
        localStorage.getItem("lang") ?? "ja"
    );

    useEffect(() => {
        localStorage.setItem("lang", lang);
    }, [lang]);

    // ...
}

ほぼ同じパターンが2回出てきます。これがあと10画面増えたら……地獄。

「使い回したいけど useState/useEffect が混じってるから関数に切り出せない」 という葛藤を解決するのが、カスタムフック。


2. カスタムフックの作り方(最小例)

import { useState, useEffect } from "react";

function useLocalStorage<T>(key: string, initialValue: T) {
    const [value, setValue] = useState<T>(() => {
        const stored = localStorage.getItem(key);
        return stored ? JSON.parse(stored) : initialValue;
    });

    useEffect(() => {
        localStorage.setItem(key, JSON.stringify(value));
    }, [key, value]);

    return [value, setValue] as const;
}

これだけ。use〜 で始まる関数」を自分で作る、それがカスタムフックです。

使う側

function SettingsPage() {
    const [theme, setTheme] = useLocalStorage("theme", "light");
    return <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>切替</button>;
}

function UserPreferencesPage() {
    const [lang, setLang] = useLocalStorage("lang", "ja");
    return <button onClick={() => setLang(lang === "ja" ? "en" : "ja")}>切替</button>;
}

1行で済むuseState を使う感覚と全く同じ。


3. カスタムフックの3つのルール

ルール1: 関数名は必ず use〜 で始める

function useFoo() { ... }    // ✅ カスタムフック
function getFoo() { ... }    // ❌ ただの関数(フックを呼べない)

なぜなら、React は use〜 で始まる関数を「フック」と認識して、フックのルール(後述)を適用するから。ESLint プラグインもこれを基準に静的解析しています。

ルール2: フックの呼び出しは「コンポーネントかカスタムフックの中」だけ

function regularFunction() {
    const [n, setN] = useState(0);  // ❌ 普通の関数の中ではダメ
}

useState / useEffect などのフックは、React 関数の中でしか呼べない。これがあるからカスタムフックを use〜 命名にする意味があります。

ルール3: 条件分岐の中でフックを呼ばない

function MyComponent({ flag }) {
    if (flag) {
        const [n, setN] = useState(0);  // ❌ 条件付き呼び出し
    }
}

React は 「フックが呼ばれる順番」 で内部の状態を管理しているので、毎回同じ順番で呼ばないと壊れます。

これらは React の 「Rules of Hooks」 と呼ばれていて、ESLint の react-hooks プラグインで自動チェックできます。導入しておくと安全。


4. 実例集

4-1. useToggle — ON/OFF を切り替える

function useToggle(initial = false) {
    const [value, setValue] = useState(initial);
    const toggle = () => setValue(v => !v);
    return [value, toggle] as const;
}

// 使い方
function Modal() {
    const [isOpen, toggle] = useToggle();
    return (
        <>
            <button onClick={toggle}>開く</button>
            {isOpen && <div>モーダル中身</div>}
        </>
    );
}

10行のフックですが、地味に何度も使います。

4-2. useFetch — API 取得を共通化

function useFetch<T>(url: string) {
    const [data, setData] = useState<T | null>(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState<Error | null>(null);

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

        fetch(url)
            .then(res => res.json())
            .then(json => {
                if (!cancelled) {
                    setData(json);
                    setLoading(false);
                }
            })
            .catch(err => {
                if (!cancelled) {
                    setError(err);
                    setLoading(false);
                }
            });

        return () => { cancelled = true; };
    }, [url]);

    return { data, loading, error };
}

// 使い方
function UserProfile({ userId }: { userId: string }) {
    const { data, loading, error } = useFetch<User>(`/api/users/${userId}`);
    if (loading) return <p>読み込み中…</p>;
    if (error) return <p>エラー: {error.message}</p>;
    return <h1>{data?.name}</h1>;
}

useFetchデータ取得・ローディング・エラー処理をひとまとめに。同じ流れを全画面で書かずに済みます。

実務では SWR / React Query などのライブラリを使うのが定番ですが、「中身は同じ構造のカスタムフック」 だと思って読むと、ライブラリの理解も早くなります。

4-3. useDebounce — 入力遅延を実装

function useDebounce<T>(value: T, delay: number) {
    const [debounced, setDebounced] = useState(value);
    useEffect(() => {
        const id = setTimeout(() => setDebounced(value), delay);
        return () => clearTimeout(id);
    }, [value, delay]);
    return debounced;
}

// 使い方: 検索ボックスで打つたびにAPI叩かないようにする
function SearchBox() {
    const [keyword, setKeyword] = useState("");
    const debounced = useDebounce(keyword, 300);

    useEffect(() => {
        if (debounced) {
            fetch(`/api/search?q=${debounced}`);
        }
    }, [debounced]);

    return <input value={keyword} onChange={e => setKeyword(e.target.value)} />;
}

入力中はAPIを叩かず、止まったら投げる」という挙動が10行で書ける。


5. 設計のコツ

5-1. 返り値は配列より「オブジェクト」が読みやすい

// 配列(useState っぽい)
const [data, loading, error] = useFetch(url);

// オブジェクト(呼び出し側が読みやすい)
const { data, loading, error } = useFetch(url);

返り値が3個以上ならオブジェクトにすると、「何が何だっけ」と迷わない。

5-2. 1つのフックに詰め込みすぎない

// ❌ 何でも入れたフック
useUserDashboard()  // ユーザー取得 + 注文取得 + 通知購読 + ...

// ✅ 役割を絞る
useUser()
useOrders()
useNotifications()

「単一責任の原則(SRP)」はカスタムフックにも適用される。Java の Service クラスと同じ感覚で。

5-3. 依存配列は明示的に

function useFetch(url: string) {
    useEffect(() => {
        // ...
    }, [url]);   // ← 依存は必ず正しく
}

ESLint の exhaustive-deps ルールが警告を出すので、それに従えば基本OK。


6. Java エンジニアへの橋渡し

React Java
カスタムフック Service クラス
useState を呼ぶ フィールドを持つ
useEffect を呼ぶ 副作用メソッドを書く
戻り値 メソッドの戻り値
Rules of Hooks Spring の Bean ライフサイクル制約

UI から離して、再利用可能なロジック単位にまとめる」というのは、レイヤーアーキテクチャと同じ思想。

UI ばかり書いてるとロジックがコンポーネントに張り付いてしまうけど、「これは Service に切り出すぞ」のノリでカスタムフック化すると、コードが急に整理されます。


7. 実務での選び方ガイド

シーン 作るべきか?
2か所以上で同じパターンが出てきた ✅ すぐに作る
useState + useEffect の組み合わせを毎回書いてる ✅ 作る
1か所でしか使わない ❌ まだ作らない
ロジックが10行未満 ❌ コンポーネント内で OK

先回り抽象化」は React でも避ける。同じパターンが2回出てから初めて切り出す、くらいがちょうどいい。


まとめ

項目 ポイント
カスタムフック use〜 で始まる、フックを呼べる自作関数
何のため ロジックの再利用、useState/useEffect の共通化
ルール use命名、コンポーネント内、条件分岐NG
設計 返り値はオブジェクト、単一責任、依存配列を正しく
作るタイミング 2か所目に登場した時

おわりに

カスタムフックを使えるようになると、React の世界では 「ロジックを書く側」 に立てます。

useState や useEffect が「部品を使う側」だとしたら、カスタムフックは「部品を組み立てる側」。

ライブラリの中身(SWR / React Query / React Hook Form など)は、ほぼ全部カスタムフックの集合体です。カスタムフックが書ける = ライブラリが読める に直結します。

次回はいよいよ React と TypeScript。型をつけて、コンポーネントを安全に書く話を整理します。React 入門編もあと2回でゴール!

0
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
0
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?