本文へスキップ
フックの決まり

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 の中
  ...
}
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」という呼ばれた順番だけです。 箱に番号を振って、順番に配っているイメージです。

デモの場合、showNicknamefalse のときは フックが 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>
  );
}