Part 4 · 状態 (state)
24 / 64 章
フックの決まり
if の中で呼んではいけない。その理由まで
useState のように use で始まるものを、 まとめてフックと呼びます。
このフックには守らないといけない決まりが 2 つあります。 たった 2 つですが、破ると直しにくい壊れ方をします。
しかもこれから先の章で「ルールを守っていれば」という前提が 何度も出てきます。先にここで決着をつけておきます。
決まりは 2 つだけ
この決まりを言い換えると、「毎回、同じフックが、同じ順番で呼ばれる」ようにしろ、 ということです。
なぜそんな決まりがあるのか。破ってみると分かります。
fixed-order.tsx
"use client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useState } from "react";
export function FixedOrder() {
// ◯ フックは、いつも関数のいちばん上に並べる。条件を付けない
const [showNickname, setShowNickname] = useState(false);
const [nickname, setNickname] = useState("");
// 出し分けるのは「呼ぶかどうか」ではなく「表示するかどうか」
if (showNickname) {
return (
<div className="flex flex-col gap-3">
<Input
placeholder="ニックネーム"
aria-label="ニックネーム"
value={nickname}
onChange={(event) => setNickname(event.target.value)}
/>
<Button size="sm" onClick={() => setShowNickname(false)}>
閉じる
</Button>
</div>
);
}
return (
<div className="flex flex-col gap-3">
<p className="text-sm text-muted-foreground">
{nickname
? `入力ずみ: ${nickname}(閉じても覚えています)`
: "ボタンを押すと、入力欄が出てきます"}
</p>
<Button size="sm" onClick={() => setShowNickname(true)}>
ニックネームを入力する
</Button>
</div>
);
}破ってみる
「ボタンを押したときだけ入力欄を出す」画面です。 入力欄が出るときだけ useState が要るのだから、if の中で呼べばいい——そう考えたくなります。
const [showNickname, setShowNickname] = useState(false);
if (showNickname) {
const [nickname, setNickname] = useState(""); // ✕ if の中
...
}ボタンを押すと、その場で壊れます
ボタンを押すと、入力欄が出てきます
押した瞬間にエラーになります。React が言ってきた内容が、そのまま箱の中に出ます。
「前回より多くのフックが呼ばれました」。数が合っていないと怒られています。
broken-order.tsx
"use client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useState } from "react";
/*
わざと決まりを破っている例。
lint は「条件分岐の中でフックを呼ぶな」と止めてくるが、
破ると何が起きるかを見せたいので、このファイルだけ黙らせている。
*/
/* eslint-disable react-hooks/rules-of-hooks */
export function BrokenOrder() {
const [showNickname, setShowNickname] = useState(false);
// ✕ if の中でフックを呼んでいる。呼ばれる回と呼ばれない回ができる
if (showNickname) {
const [nickname, setNickname] = useState("");
return (
<div className="flex flex-col gap-3">
<Input
placeholder="ニックネーム"
aria-label="ニックネーム"
value={nickname}
onChange={(event) => setNickname(event.target.value)}
/>
<Button size="sm" onClick={() => setShowNickname(false)}>
閉じる
</Button>
</div>
);
}
return (
<div className="flex flex-col gap-3">
<p className="text-sm text-muted-foreground">
ボタンを押すと、入力欄が出てきます
</p>
<Button size="sm" onClick={() => setShowNickname(true)}>
ニックネームを入力する
</Button>
</div>
);
}なぜ数と順番が大事なのか
ここが腑に落ちると、決まりを丸暗記しなくてよくなります。
React は、あなたが付けた変数名を見ていません。nickname という名前は、React には届いていないのです。
const [count, setCount] = useState(0);
const [name, setName] = useState("");React が知っているのは「1 番目の useState」「2 番目の useState」という呼ばれた順番だけです。 箱に番号を振って、順番に配っているイメージです。
デモの場合、showNickname が false のときは フックが 1 つ、true のときは 2 つ。回によって数が変わってしまいます。 React は前回と照らし合わせられなくなり、その場で止めます。
止めてくれるだけ親切です。もし止まらなければ、別の state の値が入ってくるという、 原因の見えない不具合になります。
broken-order.tsx
"use client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useState } from "react";
/*
わざと決まりを破っている例。
lint は「条件分岐の中でフックを呼ぶな」と止めてくるが、
破ると何が起きるかを見せたいので、このファイルだけ黙らせている。
*/
/* eslint-disable react-hooks/rules-of-hooks */
export function BrokenOrder() {
const [showNickname, setShowNickname] = useState(false);
// ✕ if の中でフックを呼んでいる。呼ばれる回と呼ばれない回ができる
if (showNickname) {
const [nickname, setNickname] = useState("");
return (
<div className="flex flex-col gap-3">
<Input
placeholder="ニックネーム"
aria-label="ニックネーム"
value={nickname}
onChange={(event) => setNickname(event.target.value)}
/>
<Button size="sm" onClick={() => setShowNickname(false)}>
閉じる
</Button>
</div>
);
}
return (
<div className="flex flex-col gap-3">
<p className="text-sm text-muted-foreground">
ボタンを押すと、入力欄が出てきます
</p>
<Button size="sm" onClick={() => setShowNickname(true)}>
ニックネームを入力する
</Button>
</div>
);
}直し方
フックを全部いちばん上に並べて、出し分けは表示側でやります。
// フックは無条件に、いつも同じ順で
const [showNickname, setShowNickname] = useState(false);
const [nickname, setNickname] = useState("");
// 出し分けるのは「呼ぶかどうか」ではなく「表示するかどうか」
if (showNickname) {
return <入力欄 />;
}今度は壊れません。しかも入力が残ります
ボタンを押すと、入力欄が出てきます
おまけがあります。閉じてからもう一度開くと、打った文字が残っています。 state が常にそこにあるからです。
「使わないかもしれないフックを毎回呼ぶのはむだでは?」と思うかもしれません。useState はほとんど何もしません。 箱を 1 つ用意するだけです。気にする必要はありません。
fixed-order.tsx
"use client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useState } from "react";
export function FixedOrder() {
// ◯ フックは、いつも関数のいちばん上に並べる。条件を付けない
const [showNickname, setShowNickname] = useState(false);
const [nickname, setNickname] = useState("");
// 出し分けるのは「呼ぶかどうか」ではなく「表示するかどうか」
if (showNickname) {
return (
<div className="flex flex-col gap-3">
<Input
placeholder="ニックネーム"
aria-label="ニックネーム"
value={nickname}
onChange={(event) => setNickname(event.target.value)}
/>
<Button size="sm" onClick={() => setShowNickname(false)}>
閉じる
</Button>
</div>
);
}
return (
<div className="flex flex-col gap-3">
<p className="text-sm text-muted-foreground">
{nickname
? `入力ずみ: ${nickname}(閉じても覚えています)`
: "ボタンを押すと、入力欄が出てきます"}
</p>
<Button size="sm" onClick={() => setShowNickname(true)}>
ニックネームを入力する
</Button>
</div>
);
}覚えなくても、道具が教えてくれる
うれしいことに、この 2 つの決まりはlint が自動で見張ってくれます。if の中でフックを呼んだ時点で、赤線が出ます。
React Hook "useState" is called conditionally.
React Hooks must be called in the exact same order in every component render.この見張りが働く条件が、名前です。use で始まっているものだけを、lint はフックだと見なします。
Part 6 で自分でフックを作るとき、名前を use で始めるという決まりが出てきます。 理由はこれです。名前を守ると、見張りが付いてきます。
fixed-order.tsx
"use client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useState } from "react";
export function FixedOrder() {
// ◯ フックは、いつも関数のいちばん上に並べる。条件を付けない
const [showNickname, setShowNickname] = useState(false);
const [nickname, setNickname] = useState("");
// 出し分けるのは「呼ぶかどうか」ではなく「表示するかどうか」
if (showNickname) {
return (
<div className="flex flex-col gap-3">
<Input
placeholder="ニックネーム"
aria-label="ニックネーム"
value={nickname}
onChange={(event) => setNickname(event.target.value)}
/>
<Button size="sm" onClick={() => setShowNickname(false)}>
閉じる
</Button>
</div>
);
}
return (
<div className="flex flex-col gap-3">
<p className="text-sm text-muted-foreground">
{nickname
? `入力ずみ: ${nickname}(閉じても覚えています)`
: "ボタンを押すと、入力欄が出てきます"}
</p>
<Button size="sm" onClick={() => setShowNickname(true)}>
ニックネームを入力する
</Button>
</div>
);
}理解できたか確かめる
確認クイズ
React は、どの state がどれかをどうやって見分けている?
確認クイズ
条件によって使ったり使わなかったりする state は、どう書く?
確認クイズ
自分で作る関数の名前を use で始めるのはなぜ?
broken-order.tsx
"use client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useState } from "react";
/*
わざと決まりを破っている例。
lint は「条件分岐の中でフックを呼ぶな」と止めてくるが、
破ると何が起きるかを見せたいので、このファイルだけ黙らせている。
*/
/* eslint-disable react-hooks/rules-of-hooks */
export function BrokenOrder() {
const [showNickname, setShowNickname] = useState(false);
// ✕ if の中でフックを呼んでいる。呼ばれる回と呼ばれない回ができる
if (showNickname) {
const [nickname, setNickname] = useState("");
return (
<div className="flex flex-col gap-3">
<Input
placeholder="ニックネーム"
aria-label="ニックネーム"
value={nickname}
onChange={(event) => setNickname(event.target.value)}
/>
<Button size="sm" onClick={() => setShowNickname(false)}>
閉じる
</Button>
</div>
);
}
return (
<div className="flex flex-col gap-3">
<p className="text-sm text-muted-foreground">
ボタンを押すと、入力欄が出てきます
</p>
<Button size="sm" onClick={() => setShowNickname(true)}>
ニックネームを入力する
</Button>
</div>
);
}この章のまとめ
- React が見ているのは変数名ではなく、呼ばれた順番。 だから毎回同じ順で呼ぶ必要がある
- 出し分けたいときは、呼ぶかどうかではなく、表示するかどうかを分ける
useStateは箱を用意するだけ。使わない回があっても気にしなくていいuseで始まる名前にすると、lint が決まりを見張ってくれる
fixed-order.tsx
"use client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useState } from "react";
export function FixedOrder() {
// ◯ フックは、いつも関数のいちばん上に並べる。条件を付けない
const [showNickname, setShowNickname] = useState(false);
const [nickname, setNickname] = useState("");
// 出し分けるのは「呼ぶかどうか」ではなく「表示するかどうか」
if (showNickname) {
return (
<div className="flex flex-col gap-3">
<Input
placeholder="ニックネーム"
aria-label="ニックネーム"
value={nickname}
onChange={(event) => setNickname(event.target.value)}
/>
<Button size="sm" onClick={() => setShowNickname(false)}>
閉じる
</Button>
</div>
);
}
return (
<div className="flex flex-col gap-3">
<p className="text-sm text-muted-foreground">
{nickname
? `入力ずみ: ${nickname}(閉じても覚えています)`
: "ボタンを押すと、入力欄が出てきます"}
</p>
<Button size="sm" onClick={() => setShowNickname(true)}>
ニックネームを入力する
</Button>
</div>
);
}