制御コンポーネント

Part 5 · フォーム

制御コンポーネント

input の値を React が持つということ

Part 3 の「リストと key」で、こう書きました。

「入力欄に打った文字は、React が管理している値ではありません。 ブラウザの入力欄そのものが持っている値です」。

この章では、それを React の管理下に置きます。 そうすると入力に対してできることが一気に増えます

React が値を知らない状態

入力欄をただ置くと、値を持っているのはブラウザです。 React は何が入力されたか知りません。

値を渡していない入力欄うまくいかない例

打てるが、React 側からは中身が分からない

文字は打てます。ですが React は値を知らないので、次のようなことが何ひとつできません。

  • 入力が空のときは送信ボタンを押せなくする
  • 文字数を表示する
  • ボタンを押して中身を空にする
  • 入力に応じて他の場所の表示を変える

Part 1 の言い方をすれば、入力欄の中身が状態になっていないということです。 状態でないものからは、何も決められません。

uncontrolled.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";

export function Uncontrolled() {
  // このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
  useTrackDemoRender();

  return (
    <div className="flex flex-col gap-3">
      {/* 値を React に渡していない。入力欄が自分で値を持っている */}
      <Input placeholder="名前を入力" />

      <div className="flex items-center gap-2">
        <Button size="sm" disabled>
          送信(今の値が分からないので何もできない)
        </Button>
      </div>
    </div>
  );
}

値を React に持たせる

やることは 2 つだけです。

<Input
  value={name}                                   // 表示する値は state
  onChange={(e) => setName(e.target.value)}      // 入力があれば state を更新
/>
  • value … 画面に出す値を、React が持っている state にする
  • onChange … 打たれたら、その値で state を更新する

この形を制御コンポーネントと呼びます。 入力欄の見た目を React が制御している、という意味です。

value と onChange をつなぐ直した例

文字数も、ボタンの状態も、クリアもできる

0 文字

値が state になった途端、Part 4 でやったことがすべて使えるようになりました。 文字数は name.length で計算できますし、 送信できるかどうかも name.trim() !== "" で求まります。どちらも state にする必要はありません。

controlled.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useState } from "react";

export function Controlled() {
  // このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
  useTrackDemoRender();

  const [name, setName] = useState("");

  // 入力が空かどうかは、state から計算できる
  const canSubmit = name.trim() !== "";

  return (
    <div className="flex flex-col gap-3">
      <Input
        placeholder="名前を入力"
        // 表示する値は React が持っているものにする
        value={name}
        // 入力があったら、React 側の値を更新する
        onChange={(event) => setName(event.target.value)}
      />

      <div className="flex flex-wrap items-center gap-2">
        <Button size="sm" disabled={!canSubmit}>
          送信
        </Button>
        <Button size="sm" variant="outline" onClick={() => setName("")}>
          クリア
        </Button>
        <span className="text-muted-foreground">{name.length} 文字</span>
      </div>
    </div>
  );
}

一周まわって表示されている

この仕組みは、慣れるまで奇妙に見えます。 打った文字がそのまま出ているのではなく、一周まわってから表示されているからです。

キーを打つ
  → onChange が呼ばれる
  → setName で state が変わる
  → コンポーネントが実行し直される
  → value={name} に新しい値が入る
  → 画面に文字が出る

遠回りに見えますが、これがあるおかげで入力を途中で加工できます

// 数字以外は受け付けない
onChange={(e) => setTel(e.target.value.replace(/[^0-9]/g, ""))}

// 常に大文字にする
onChange={(e) => setCode(e.target.value.toUpperCase())}

画面に出る値は最後まで React が決めているので、打った内容と違うものを表示することもできるわけです。

controlled.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useState } from "react";

export function Controlled() {
  // このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
  useTrackDemoRender();

  const [name, setName] = useState("");

  // 入力が空かどうかは、state から計算できる
  const canSubmit = name.trim() !== "";

  return (
    <div className="flex flex-col gap-3">
      <Input
        placeholder="名前を入力"
        // 表示する値は React が持っているものにする
        value={name}
        // 入力があったら、React 側の値を更新する
        onChange={(event) => setName(event.target.value)}
      />

      <div className="flex flex-wrap items-center gap-2">
        <Button size="sm" disabled={!canSubmit}>
          送信
        </Button>
        <Button size="sm" variant="outline" onClick={() => setName("")}>
          クリア
        </Button>
        <span className="text-muted-foreground">{name.length} 文字</span>
      </div>
    </div>
  );
}

他の入力欄でも同じ

// チェックボックスは checked
<input
  type="checkbox"
  checked={isAgreed}
  onChange={(e) => setIsAgreed(e.target.checked)}
/>

// 選択肢も value
<select value={role} onChange={(e) => setRole(e.target.value)}>
  <option value="admin">管理者</option>
</select>

チェックボックスだけ value ではなく checked を使います。 受け取る側も e.target.checked になります。 それ以外の考え方は同じです。

controlled.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useState } from "react";

export function Controlled() {
  // このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
  useTrackDemoRender();

  const [name, setName] = useState("");

  // 入力が空かどうかは、state から計算できる
  const canSubmit = name.trim() !== "";

  return (
    <div className="flex flex-col gap-3">
      <Input
        placeholder="名前を入力"
        // 表示する値は React が持っているものにする
        value={name}
        // 入力があったら、React 側の値を更新する
        onChange={(event) => setName(event.target.value)}
      />

      <div className="flex flex-wrap items-center gap-2">
        <Button size="sm" disabled={!canSubmit}>
          送信
        </Button>
        <Button size="sm" variant="outline" onClick={() => setName("")}>
          クリア
        </Button>
        <span className="text-muted-foreground">{name.length} 文字</span>
      </div>
    </div>
  );
}

理解できたか確かめる

確認クイズ

制御コンポーネントにすると何ができるようになる?

確認クイズ

入力欄に文字を打っても何も表示されない。原因は?

controlled.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useState } from "react";

export function Controlled() {
  // このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
  useTrackDemoRender();

  const [name, setName] = useState("");

  // 入力が空かどうかは、state から計算できる
  const canSubmit = name.trim() !== "";

  return (
    <div className="flex flex-col gap-3">
      <Input
        placeholder="名前を入力"
        // 表示する値は React が持っているものにする
        value={name}
        // 入力があったら、React 側の値を更新する
        onChange={(event) => setName(event.target.value)}
      />

      <div className="flex flex-wrap items-center gap-2">
        <Button size="sm" disabled={!canSubmit}>
          送信
        </Button>
        <Button size="sm" variant="outline" onClick={() => setName("")}>
          クリア
        </Button>
        <span className="text-muted-foreground">{name.length} 文字</span>
      </div>
    </div>
  );
}

この章のまとめ

  • 入力欄をただ置くと、値を持つのはブラウザ。React は中身を知らない
  • valueonChange をつなぐと、 値が state になる(制御コンポーネント
  • state になれば、文字数も送信可否も計算で求められる
  • 打った文字は一周まわって表示される。だから途中で加工できる
  • value だけ渡して onChange を忘れると、打てない入力欄になる

controlled.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useState } from "react";

export function Controlled() {
  // このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
  useTrackDemoRender();

  const [name, setName] = useState("");

  // 入力が空かどうかは、state から計算できる
  const canSubmit = name.trim() !== "";

  return (
    <div className="flex flex-col gap-3">
      <Input
        placeholder="名前を入力"
        // 表示する値は React が持っているものにする
        value={name}
        // 入力があったら、React 側の値を更新する
        onChange={(event) => setName(event.target.value)}
      />

      <div className="flex flex-wrap items-center gap-2">
        <Button size="sm" disabled={!canSubmit}>
          送信
        </Button>
        <Button size="sm" variant="outline" onClick={() => setName("")}>
          クリア
        </Button>
        <span className="text-muted-foreground">{name.length} 文字</span>
      </div>
    </div>
  );
}