React.js

ReactのuseId入門:ラベルと説明を関連付けた入力フォームを作る

この記事でわかること

ReactのuseIdで重複しないIDを生成し、入力欄・ラベル・説明文を関連付ける方法を解説。TypeScriptのフォーム例、keyとの違い、SSR時の注意点を学べます。

フォーム部品を複数並べると、入力欄のIDが重複しやすくなります。useIdはReactのコンポーネント内でIDを生成し、ラベルや説明文との関連付けに使うHookです。この記事では、同じ部品を2つ配置しても関連付けが混ざらないフォームを作ります。

useIdが役立つ場面

ラベルと入力欄を結び付ける

HTMLでは、labelforと入力欄のidを一致させることで関連付けます。ReactのJSXではforの代わりにhtmlForを指定します。説明文を関連付ける場合は、説明要素のIDをaria-describedbyに渡します。

同じ部品の中に固定のIDを書いてしまうと、部品を複数使ったときにIDが重複します。部品ごとにuseIdで生成した文字列を使えば、その問題を避けやすくなります。

この記事の環境と範囲

ViteのReact・TypeScriptプロジェクトで実行します。環境構築はReact.js入門を参照してください。例はReact 18でも使える記法です。フォームは入力と確認メッセージの表示までを行い、サーバーへ送信しません。

再利用できる入力部品を作る

NameField.tsxを実装する

src/NameField.tsxを作成します。生成したIDを入力欄に、その末尾へ-hintを付けたIDを説明文に使用します。

import { useId } from 'react';

type NameFieldProps = {
  label: string;
  name: string;
  hint: string;
  value: string;
  onChange: (value: string) => void;
};

export function NameField({ label, name, hint, value, onChange }: NameFieldProps) {
  const inputId = useId();
  const hintId = `${inputId}-hint`;

  return (
    <div>
      <label htmlFor={inputId}>{label}</label>
      <input
        id={inputId}
        name={name}
        type="text"
        value={value}
        onChange={(event) => onChange(event.target.value)}
        aria-describedby={hintId}
        required
        maxLength={40}
      />
      <p id={hintId}>{hint}</p>
    </div>
  );
}

idとnameの役割を分ける

idはページ内の要素を識別するための値です。nameはフォームデータの項目名になります。今回の部品では、IDはuseIdで生成し、項目名は親から渡しています。

入力値も親から受け取り、変更時にonChangeで通知します。useIdは入力値を保存する機能ではありません。

同じ部品を2つ並べる

App.tsxにフォームを配置する

src/App.tsxを次の内容にします。申込者と同行者の入力値を別々のstateで保持します。src/main.tsxはViteのテンプレートのままで構いません。

import { useState, type FormEvent } from 'react';
import { NameField } from './NameField';

export default function App() {
  const [applicant, setApplicant] = useState('');
  const [companion, setCompanion] = useState('');
  const [message, setMessage] = useState('');

  function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    if (!applicant.trim() || !companion.trim()) {
      setMessage('両方の名前を入力してください。空白だけでは登録できません。');
      return;
    }
    setMessage(`${applicant.trim()}さんと${companion.trim()}さんの入力を確認しました。`);
  }

  return (
    <main>
      <h1>useIdで作る参加者フォーム</h1>
      <form onSubmit={handleSubmit}>
        <NameField
          label="申込者の名前"
          name="applicant"
          hint="申込者の名前を40文字以内で入力してください。"
          value={applicant}
          onChange={(value) => { setApplicant(value); setMessage(''); }}
        />
        <NameField
          label="同行者の名前"
          name="companion"
          hint="同行者の名前を40文字以内で入力してください。"
          value={companion}
          onChange={(value) => { setCompanion(value); setMessage(''); }}
        />
        <button type="submit">入力を確認する</button>
      </form>
      <p role="status">{message}</p>
    </main>
  );
}

入力チェックの範囲を理解する

requiredは空の入力に対するブラウザーのチェックです。空白だけの文字列は別途trim()で確認しています。実際にサーバーへ送信する場合は、サーバー側でも必須項目や文字数などを検証します。

この例のrole="status"は操作結果を通知するために使います。入力エラーが多いフォームでは、各入力欄とのエラー文の関連付けや、エラー箇所への移動も検討してください。

動作確認とIDの使い方

2つのラベルを確認する

  • 申込者のラベルをクリックすると、申込者の入力欄にフォーカスします。
  • 同行者のラベルは、同行者の入力欄と関連付いています。
  • 開発者ツールで、2つの入力欄のIDが異なることを確認します。
  • 各入力欄のaria-describedbyが、その欄の説明文のIDを参照していることを確認します。
  • 入力を確認した後で名前を変更すると、前の確認メッセージが消えます。

支援技術での読み上げは、使うブラウザーとスクリーンリーダーでも確認してください。DOM上の関連付けが正しいことと、利用者にとって使いやすいことは、両方を確認する必要があります。

一覧のkeyには使わない

useIdは商品や記事のデータIDを作るための機能ではありません。一覧のkeyにはデータが持つ安定したIDを使います。また、ループの中でuseIdを呼び出さず、必要なら部品を分割して、その部品のトップレベルで呼び出します。

サーバー描画では構造をそろえる

サーバー描画とクライアントの初期描画では、コンポーネントの構造をそろえる必要があります。useIdがあっても、サーバーとブラウザーで違う部品を描画する問題を自動的に修正できるわけではありません。

生成された文字列の書式を解析して業務データに使わないでください。独立したReactのルートを一つのページに複数作る場合は、identifierPrefixの設定も公式資料で確認します。

参考資料と次の学習

stateやpropsと組み合わせる

フォームの値はuseState、親子間の受け渡しはprops、要素同士の関連付けにはuseIdというように役割を分けます。基本Hooks入門も合わせて読むと、値とDOMの役割を整理できます。

公式ドキュメント

y.
WRITTEN BY

y_ymo10

SEの部屋で、JavaScript・TypeScript・React.js・Next.jsの開発ノートを公開しています。

ほかの開発ノートを読む →