@k8ordo/form

エラーを表示する

ブラウザが入力を確かめて見つけた誤りも、サーバーが検証して返した誤りも、同じerrorに届きます。このページでは、エラーをどこに表示するかと、送信に失敗したときにフォーカスがどう動くかを説明します。

このページの内容

入力欄にエラーを表示する

field()が返す値のうち、errorにはその欄のエラーの文言が、invalidには失敗しているかどうかが入ります。

tsx
const title = form.field('title');

<label htmlFor="title">Title</label>
<input {...title.input} aria-describedby="title-error" id="title" />
{title.error !== undefined && <p id="title-error">{title.error}</p>}

エラーが出るのは、入力欄から離れたときと送信したときです。入力中にいきなり出ることはなく、正しい値に直すと消えます。

文言を変える

表示される文言は、zodが出すエラーメッセージそのものです。文言を変えたいときは、スキーマの側に書きます。

ts
z.object({
  title: z.string().min(1, 'Enter a title').max(120, 'Keep it to 120 characters'),
});

ブラウザでの検証もサーバーでの検証も同じスキーマから文言を取り出すので、どちらで見つかっても同じ言葉で伝わります。

リクエストの言語に合わせて文言を切り替えるなら、文言を関数で渡し、formFieldsを描画の中で呼んでください。モジュールのトップレベルで呼ぶと、最初に呼ばれたときの言語で固定されてしまいます。

page.tsx
const talkSchema = z.object({
  title: z.string().min(1, { error: () => m.talk.titleMissing() }),
});

export default function NewTalkPage() {
  const talkFields = formFields(talkSchema);
  return <TalkForm action={createTalk} fields={talkFields} />;
}

失敗した入力欄にフォーカスを移す

送信に失敗すると、ページの中で最初に失敗した入力欄へフォーカスが移ります。このために書くコードはありません。

ここでいう「最初」は、ページに並んでいる順番です。スキーマに書いたキーの順番ではありません。

警告

落とし穴

自分で組み立てたstateを返すときは、新しいtokenを入れてください。同じ内容の失敗が2回続いたとき、tokenが同じだと同じ返事とみなされ、フォーカスが移りません。

フォーム全体のエラーを表示する

どの入力欄にも属さないエラーは、form.formErrorに入ります。たとえば、pathを指定せずにスキーマ全体へ付けた.refine()のエラーがそうです。

tsx
<form {...form.props} action={formAction}>
  {form.formError.message !== undefined && (
    <p {...form.formError.props}>{form.formError.message}</p>
  )}
  {/* fields */}
</form>

表示する要素にはformError.propsを展開します。要素がフォーカスを受け取れるようになるので、スクリーンリーダーにもエラーが伝わります。

表示する場所は、入力欄より上にします。入力欄の失敗と同時に起きても先にこちらへフォーカスが移り、そこからTabキーで入力欄へ進めます。

サーバーでしか分からない失敗を返す

タイトルがすでに使われている、といった失敗は、データベースを見るまで分かりません。parseFormで検証を通ったあとにこうした失敗が見つかったら、parsed.stateにエラーを足して返します。

actions.ts
export async function createTalk(
  _prev: FormState,
  formData: FormData,
) {
  const parsed = parseForm(talkSchema, formData);
  if (!parsed.success) return parsed.state;

  if (await titleExists(parsed.data.title)) {
    return {
      ...parsed.state,
      errors: { title: 'A talk with this title already exists' },
    };
  }

  await saveTalk(parsed.data);
  redirect(href('/talks'));
}

parsed.stateには、入力されていた値と新しいtokenがすでに入っています。そのため値は入力欄に戻り、フォーカスもエラーの出た欄に移ります。

Playground

エラーの出方を試す

ハンドルとパスワードを登録するフォームです。表示される文言は、どれもページのスキーマから作っています。

試してみる

  1. ハンドルにabと入力して欄から離れると、3文字以上にするよう求めるエラーが出ます。
  2. 確認用のパスワードを違う値にすると、一致しないというエラーが出ます。
  3. 空の欄を残したまま「登録」を押すと、最初に失敗した欄へフォーカスが移ります。
k8ordo

Baselineに入った機能を、制限なく使うReactのライブラリ群。

© 2026 k8o — MIT License

組版:Noto Sans JP / M PLUS 2