レビュー待ち·難易度: 初級·更新: 2026-04-18

制御コンポーネントと非制御コンポーネントの違いと使い分け

useState で値を管理する制御コンポーネントと、useRef で DOM を直接参照する非制御コンポーネントの違いを実装例で比較し、それぞれの適切なユースケースを示す。

nextjsform

対応バージョン

nextjs 15react 19

前提環境

React の useState と useRef の基本を理解していること

概要

useState で値を React が管理する「制御コンポーネント」と、useRef で DOM を直接参照する「非制御コンポーネント」の違いを実装例で比較する。それぞれの動作の違いと、どちらを選ぶべきかの判断基準を示す。

インストール

# 追加インストールは不要

実装

制御コンポーネント(Controlled)

// components/ControlledForm.tsx
"use client";

import { useState } from "react";

export function ControlledForm() {
  const [name, setName] = useState("");
  const [submitted, setSubmitted] = useState<string | null>(null);

  function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setSubmitted(name);
  }

  return (
    <form onSubmit={handleSubmit} className="space-y-3">
      <div>
        <label className="block text-sm font-medium text-gray-700">名前</label>
        {/* value を state で管理しているため、入力のたびに再レンダリングされる */}
        <input
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
          className="mt-1 block w-full rounded border px-3 py-2 text-sm"
        />
        {/* リアルタイムで入力値を参照できる */}
        <p className="mt-1 text-xs text-gray-500">入力中: {name}</p>
      </div>
      <button
        type="submit"
        disabled={name.trim() === ""}
        className="rounded bg-blue-600 px-4 py-2 text-sm text-white disabled:opacity-50"
      >
        送信
      </button>
      {submitted && (
        <p className="text-sm text-green-600">送信されました: {submitted}</p>
      )}
    </form>
  );
}

非制御コンポーネント(Uncontrolled)

// components/UncontrolledForm.tsx
"use client";

import { useRef, useState } from "react";

export function UncontrolledForm() {
  const nameRef = useRef<HTMLInputElement>(null);
  const [submitted, setSubmitted] = useState<string | null>(null);

  function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    // 送信時にのみ DOM から値を取得する
    const value = nameRef.current?.value ?? "";
    if (value.trim()) setSubmitted(value);
  }

  function handleReset() {
    // DOM を直接操作してリセット
    if (nameRef.current) nameRef.current.value = "";
    setSubmitted(null);
  }

  return (
    <form onSubmit={handleSubmit} className="space-y-3">
      <div>
        <label className="block text-sm font-medium text-gray-700">名前</label>
        {/* defaultValue で初期値のみ設定。以降は React が管理しない */}
        <input
          type="text"
          defaultValue=""
          ref={nameRef}
          className="mt-1 block w-full rounded border px-3 py-2 text-sm"
        />
        {/* 入力中の値はリアルタイムに参照できない */}
      </div>
      <div className="flex gap-2">
        <button
          type="submit"
          className="rounded bg-blue-600 px-4 py-2 text-sm text-white"
        >
          送信
        </button>
        <button
          type="button"
          onClick={handleReset}
          className="rounded border px-4 py-2 text-sm text-gray-700"
        >
          リセット
        </button>
      </div>
      {submitted && (
        <p className="text-sm text-green-600">送信されました: {submitted}</p>
      )}
    </form>
  );
}

比較表

観点制御コンポーネント非制御コンポーネント
値の管理React state(useStateDOM(useRef
入力中の値参照できる(即時)できない(取得時のみ)
バリデーション入力のたびに実行できる送信時に実行
再レンダリング入力のたびに発生発生しない
向いている用途入力に応じて UI を変える場面シンプルな送信フォーム

ポイント

  • 制御コンポーネントは value + onChange で React が値を所有するため、入力のたびに再レンダリングが走る。リアルタイムバリデーションや入力依存の UI に適している
  • 非制御コンポーネントは defaultValue + ref を使い、DOM が値を所有する。送信時にのみ値を読み取れば十分なシンプルなフォームで再レンダリングを減らせる
  • react-hook-form は内部で非制御コンポーネントを採用し、登録(register)した input を ref で管理することで大量フィールドでのパフォーマンスを確保している
  • ファイル input(<input type="file">)は常に非制御。value で制御できないため refe.target.files で取得する
  • どちらを選ぶかの基準: 入力中の値に基づいて何かする必要があれば制御、なければ非制御で十分

注意点

react-hook-form は内部で非制御コンポーネントを採用している。この記事で制御/非制御の仕組みを理解することで、ライブラリの設計意図も把握しやすくなる。

関連サンプル