本文へスキップ
ライブラリの選び方

Part 10 · 実務で使う道具

58 / 64

ライブラリの選び方

入れる前に何を見るか。この教材で使ったものの一覧

この教材で、いくつものライブラリを使ってきました。 nuqs、use-local-storage-state、SWR、 React Hook Form、zod、use-debounce。

どれも自分で書けるものです。 実際、useDebounce の中身は数行でした。

では、どこで線を引くのか。その話をして終わります。

入れることには値段がある

ライブラリを 1 つ入れると、次のものが付いてきます。

  • 読む人が覚えることが 1 つ増える
  • 更新の面倒が 1 つ増える。 React が上がったとき、これも追いつく必要がある
  • 壊れたときに自分で直せない可能性
  • ブラウザに送られるコードが増える

だから「便利そうだから入れる」は理由になりません。 払う値段に見合う困りごとが、先にある必要があります。

3 つめが実は決定的です。並べてみます。

  • 気づける間違い メールアドレスの形を間違えて判定していれば、 試した瞬間に分かります。直せます
  • 気づけない間違い Part 9 で見た「古い結果が後から届いて上書きする」。たまにしか起きず、再現もできません

自分で書いてよいのは前者です。後者を自分で正しく書き切るのは、かなり難しい。SWR のような道具に任せる価値があるのは、まさにここです。

useWindowWidth のようなものは、 間違えても影響がその 1 か所に閉じます。 これも自分で書いてよい側です。

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;
}

入れると決めたら、何を見るか

同じことをするライブラリは、たいてい複数あります。 選ぶときに見るのは、だいたいこの 4 つです。

見るもの何が分かるか
最終更新日1 年以上前で止まっていたら、React の新しい版で 動かなくなる恐れがある
未解決の Issue数より中身。困っている人に返事があるか
週あたりの利用数多いほど、困ったときに答えが見つかる
TypeScript の型同梱されているか。別パッケージなら一手間増える

抜けられるかどうか

もう 1 つ、見落とされがちな観点があります。やめたくなったとき、どれくらい大変かです。

// 抜けやすい: 使っている場所が限られる
const [debounced] = useDebounce(keyword, 500);

// 抜けにくい: アプリ全体の書き方がそれに合わせて決まる
// (状態管理ライブラリ、ルーティング、UI フレームワーク)

抜けやすいものは、気軽に入れてかまいません。抜けにくいものほど、慎重に選びます。

debounced.tsx

"use client";

import { RenderBox } from "@/components/lesson/render-box";
import { Input } from "@/components/ui/input";
import { memo, useEffect, useState } from "react";
import { useDebounce } from "use-debounce";

/*
  本来ここは「API を呼んで、返ってきた結果を state に入れる」処理。
  デモでは通信の代わりに回数だけ数えている。
  lint は「effect の中で直接 setState するな」と止めてくるが、
  実物では await を挟むので、この形自体は現実のコードに近い。
*/
/* eslint-disable react-hooks/set-state-in-effect */

// 検索の回数が変わったときだけ描き直される
const SearchBox = memo(function SearchBox({ count }: { count: number }) {
  return (
    <RenderBox title="落ち着いてから検索">
      検索した回数: <strong>{count}</strong>
    </RenderBox>
  );
});

export function Debounced() {
  const [keyword, setKeyword] = useState("");

  // 打つのが 500ms 止まってから、こちらの値が追いつく
  const [debouncedKeyword] = useDebounce(keyword, 500);

  const [searchCount, setSearchCount] = useState(0);

  useEffect(() => {
    if (!debouncedKeyword) return;

    // 見張るのは「落ち着いたほうの値」
    setSearchCount((count) => count + 1);
  }, [debouncedKeyword]);

  return (
    <div className="flex flex-col gap-3">
      <Input
        placeholder="検索してみる"
        aria-label="検索してみる"
        value={keyword}
        onChange={(event) => setKeyword(event.target.value)}
      />

      <p className="text-sm text-muted-foreground">
        入力欄: {keyword || "(空)"} / 検索に使う値:{" "}
        {debouncedKeyword || "(空)"}
      </p>

      {/* 検索が走ったときだけ光らせたいので、打鍵の巻き添えを memo で切る */}
      <SearchBox count={searchCount} />
    </div>
  );
}

この教材で使ったもの

それぞれ、どの困りごとに対する答えだったかを並べておきます。

名前困りごと
nuqsURL の読み書きが、型変換込みで面倒Part 9
use-local-storage-state素で書くとサーバー側で落ちる。保存の書き忘れPart 9
swr読み込み中・失敗・取り直し・重複防止・順序の入れ替わりPart 9
react-hook-form項目が増えたフォーム。1 文字ごとの描き直しPart 10
zod型と検査がずれる。外から来た値を確かめられないPart 10
use-debounce打つたびに処理が走るPart 10

どれも「困りごとが先」です。ライブラリを知ってから使い道を探したものは、1 つもありません。

debounced.tsx

"use client";

import { RenderBox } from "@/components/lesson/render-box";
import { Input } from "@/components/ui/input";
import { memo, useEffect, useState } from "react";
import { useDebounce } from "use-debounce";

/*
  本来ここは「API を呼んで、返ってきた結果を state に入れる」処理。
  デモでは通信の代わりに回数だけ数えている。
  lint は「effect の中で直接 setState するな」と止めてくるが、
  実物では await を挟むので、この形自体は現実のコードに近い。
*/
/* eslint-disable react-hooks/set-state-in-effect */

// 検索の回数が変わったときだけ描き直される
const SearchBox = memo(function SearchBox({ count }: { count: number }) {
  return (
    <RenderBox title="落ち着いてから検索">
      検索した回数: <strong>{count}</strong>
    </RenderBox>
  );
});

export function Debounced() {
  const [keyword, setKeyword] = useState("");

  // 打つのが 500ms 止まってから、こちらの値が追いつく
  const [debouncedKeyword] = useDebounce(keyword, 500);

  const [searchCount, setSearchCount] = useState(0);

  useEffect(() => {
    if (!debouncedKeyword) return;

    // 見張るのは「落ち着いたほうの値」
    setSearchCount((count) => count + 1);
  }, [debouncedKeyword]);

  return (
    <div className="flex flex-col gap-3">
      <Input
        placeholder="検索してみる"
        aria-label="検索してみる"
        value={keyword}
        onChange={(event) => setKeyword(event.target.value)}
      />

      <p className="text-sm text-muted-foreground">
        入力欄: {keyword || "(空)"} / 検索に使う値:{" "}
        {debouncedKeyword || "(空)"}
      </p>

      {/* 検索が走ったときだけ光らせたいので、打鍵の巻き添えを memo で切る */}
      <SearchBox count={searchCount} />
    </div>
  );
}

理解できたか確かめる

確認クイズ

自分で書くか、ライブラリに任せるか。決め手になるのは?

確認クイズ

GitHub の星が多いライブラリは安全?

確認クイズ

慎重に選ぶべきなのはどんなライブラリ?

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;
}

この章のまとめ

  • ライブラリには値段がある。 覚えること、更新、直せないリスク
  • 判断はそもそも要るか → 何行で書けるか → 間違いに気づけるかの順
  • 選ぶときは最終更新日を最初に見る。星の数ではない
  • 抜けにくいものほど慎重に選ぶ
  • 困りごとが先、ライブラリは後

debounced.tsx

"use client";

import { RenderBox } from "@/components/lesson/render-box";
import { Input } from "@/components/ui/input";
import { memo, useEffect, useState } from "react";
import { useDebounce } from "use-debounce";

/*
  本来ここは「API を呼んで、返ってきた結果を state に入れる」処理。
  デモでは通信の代わりに回数だけ数えている。
  lint は「effect の中で直接 setState するな」と止めてくるが、
  実物では await を挟むので、この形自体は現実のコードに近い。
*/
/* eslint-disable react-hooks/set-state-in-effect */

// 検索の回数が変わったときだけ描き直される
const SearchBox = memo(function SearchBox({ count }: { count: number }) {
  return (
    <RenderBox title="落ち着いてから検索">
      検索した回数: <strong>{count}</strong>
    </RenderBox>
  );
});

export function Debounced() {
  const [keyword, setKeyword] = useState("");

  // 打つのが 500ms 止まってから、こちらの値が追いつく
  const [debouncedKeyword] = useDebounce(keyword, 500);

  const [searchCount, setSearchCount] = useState(0);

  useEffect(() => {
    if (!debouncedKeyword) return;

    // 見張るのは「落ち着いたほうの値」
    setSearchCount((count) => count + 1);
  }, [debouncedKeyword]);

  return (
    <div className="flex flex-col gap-3">
      <Input
        placeholder="検索してみる"
        aria-label="検索してみる"
        value={keyword}
        onChange={(event) => setKeyword(event.target.value)}
      />

      <p className="text-sm text-muted-foreground">
        入力欄: {keyword || "(空)"} / 検索に使う値:{" "}
        {debouncedKeyword || "(空)"}
      </p>

      {/* 検索が走ったときだけ光らせたいので、打鍵の巻き添えを memo で切る */}
      <SearchBox count={searchCount} />
    </div>
  );
}