本文へスキップ
カスタムフック

Part 6 · 副作用 (useEffect)

39 / 64

カスタムフック

自分でフックを作る。ただの関数の切り出し

ここまで useStateuseEffectuseReducer と、いくつものフックを使ってきました。

これらはReact が用意したフックです。 そして自分でフックを作ることもできます

新しい概念ではありません。ただの関数の切り出しです。

同じものが 2 回書かれている

画面幅を使う部品が 2 つあります。 (ブラウザの幅を変えると、両方の数字が変わります)

同じ 8 行が 2 か所にあるうまくいかない例

ウィンドウの幅を変えると、2 つとも光る

WidthLabel
いまの画面幅: 0px
DeviceLabel
判定: スマホ向け

動きます。問題は、右のコードを見ると分かります。まったく同じ 8 行が、2 回書かれています。

const [width, setWidth] = useState(0);

useEffect(() => {
  const update = () => setWidth(window.innerWidth);
  update();
  window.addEventListener("resize", update);
  return () => window.removeEventListener("resize", update);
}, []);

3 つめの部品でも幅が要るなら、また 8 行。後片付けの書き忘れが 1 か所でもあれば、そこだけ漏れます。

duplicated.tsx

"use client";

import { RenderBox } from "@/components/lesson/render-box";
import { useEffect, useState } from "react";

function WidthLabel() {
  // ここから 8 行
  const [width, setWidth] = useState(0);

  useEffect(() => {
    const update = () => setWidth(window.innerWidth);

    update();
    window.addEventListener("resize", update);
    return () => window.removeEventListener("resize", update);
  }, []);
  // ここまで

  return <RenderBox title="WidthLabel">いまの画面幅: {width}px</RenderBox>;
}

function DeviceLabel() {
  // まったく同じ 8 行が、もう一度
  const [width, setWidth] = useState(0);

  useEffect(() => {
    const update = () => setWidth(window.innerWidth);

    update();
    window.addEventListener("resize", update);
    return () => window.removeEventListener("resize", update);
  }, []);

  return (
    <RenderBox title="DeviceLabel">
      判定: {width < 768 ? "スマホ向け" : "パソコン向け"}
    </RenderBox>
  );
}

export function Duplicated() {
  return (
    <div className="flex flex-col gap-2">
      <WidthLabel />
      <DeviceLabel />
    </div>
  );
}

関数に切り出す

重複したコードを関数にまとめるのは、React に限らず普通のことです。 ここでも同じことをします。

まるごとコピーして、関数で包み、最後に return を付けただけです。 中身は一切変えていません。

useWindowWidth の中では useStateuseEffect を呼んでいます。フックの中でフックを呼べる——これがカスタムフックの正体です。

use-window-width.ts

"use client";

import { useEffect, useState } from "react";

/**
 * 画面幅を返すカスタムフック。
 *
 * 名前が use で始まっていて、中で他のフックを呼んでいる。
 * それだけで「カスタムフック」と呼ばれる。特別な仕組みはない。
 */
export function useWindowWidth() {
  const [width, setWidth] = useState(0);

  useEffect(() => {
    const update = () => setWidth(window.innerWidth);

    update();
    window.addEventListener("resize", update);

    // 後片付けもここに置いておける。使う側は知らなくてよい
    return () => window.removeEventListener("resize", update);
  }, []);

  return width;
}

使う側

const width = useWindowWidth();

8 行が 1 行になりました。イベントの登録も、後片付けも、 使う側はもう知らなくてよくなっています。

カスタムフックにまとめた直した例

動きも光り方も、まったく同じ

WidthLabel
いまの画面幅: 0px
DeviceLabel
判定: スマホ向け

画面の動きは 1 ミリも変わっていません。変わったのは、読む人が背負う量です。

extracted.tsx

"use client";

import { RenderBox } from "@/components/lesson/render-box";
import { useWindowWidth } from "./use-window-width";

function WidthLabel() {
  // 8 行が 1 行になった
  const width = useWindowWidth();

  return <RenderBox title="WidthLabel">いまの画面幅: {width}px</RenderBox>;
}

function DeviceLabel() {
  const width = useWindowWidth();

  return (
    <RenderBox title="DeviceLabel">
      判定: {width < 768 ? "スマホ向け" : "パソコン向け"}
    </RenderBox>
  );
}

export function Extracted() {
  return (
    <div className="flex flex-col gap-2">
      <WidthLabel />
      <DeviceLabel />
    </div>
  );
}

いつ切り出すか

2 回目に同じものを書いたときが、いちばん分かりやすい目安です。 1 回目で切り出すと、まだ形が定まっていないことが多い。

もう 1 つ、重複していなくても切り出す価値がある場合があります。

  • コンポーネントが読みにくくなっているとき。 画面の組み立てと、裏側の処理が混ざっている
  • 名前を付けたら意味がはっきりするときuseWindowWidth と書いてあれば、中を読まずに済む

use-window-width.ts

"use client";

import { useEffect, useState } from "react";

/**
 * 画面幅を返すカスタムフック。
 *
 * 名前が use で始まっていて、中で他のフックを呼んでいる。
 * それだけで「カスタムフック」と呼ばれる。特別な仕組みはない。
 */
export function useWindowWidth() {
  const [width, setWidth] = useState(0);

  useEffect(() => {
    const update = () => setWidth(window.innerWidth);

    update();
    window.addEventListener("resize", update);

    // 後片付けもここに置いておける。使う側は知らなくてよい
    return () => window.removeEventListener("resize", update);
  }, []);

  return width;
}

理解できたか確かめる

確認クイズ

カスタムフックの決まりは?

確認クイズ

2 つの部品が同じカスタムフックを呼ぶと、state は?

確認クイズ

切り出す目安として適切なのは?

use-window-width.ts

"use client";

import { useEffect, useState } from "react";

/**
 * 画面幅を返すカスタムフック。
 *
 * 名前が use で始まっていて、中で他のフックを呼んでいる。
 * それだけで「カスタムフック」と呼ばれる。特別な仕組みはない。
 */
export function useWindowWidth() {
  const [width, setWidth] = useState(0);

  useEffect(() => {
    const update = () => setWidth(window.innerWidth);

    update();
    window.addEventListener("resize", update);

    // 後片付けもここに置いておける。使う側は知らなくてよい
    return () => window.removeEventListener("resize", update);
  }, []);

  return width;
}

この章のまとめ

  • カスタムフックはただの関数の切り出し。 新しい仕組みではない
  • 決まりは名前を use で始めることだけ
  • 中で他のフックを呼べる。後片付けも中に閉じ込められる
  • 共有されるのはロジックだけで、state は別々
  • 世の中の use〜 ライブラリも、全部これ

use-window-width.ts

"use client";

import { useEffect, useState } from "react";

/**
 * 画面幅を返すカスタムフック。
 *
 * 名前が use で始まっていて、中で他のフックを呼んでいる。
 * それだけで「カスタムフック」と呼ばれる。特別な仕組みはない。
 */
export function useWindowWidth() {
  const [width, setWidth] = useState(0);

  useEffect(() => {
    const update = () => setWidth(window.innerWidth);

    update();
    window.addEventListener("resize", update);

    // 後片付けもここに置いておける。使う側は知らなくてよい
    return () => window.removeEventListener("resize", update);
  }, []);

  return width;
}