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>
);
}この教材で使ったもの
それぞれ、どの困りごとに対する答えだったかを並べておきます。
| 名前 | 困りごと | 章 |
|---|---|---|
| nuqs | URL の読み書きが、型変換込みで面倒 | 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>
);
}