zukucode
主にWEB関連の情報を技術メモとして発信しています。

React 入力値の重複チェックをデバウンスして実行する

ユーザー名の登録フォームでは、入力した値がすでに使用されているかAPIで確認することがあります。

入力のたびにAPIを呼び出すとリクエストが増えるため、入力が止まってから重複チェックを実行します。

今回は形式チェック、デバウンス、APIによる重複チェックを組み合わせる方法を紹介します。

重複チェックは登録可否を保証する処理ではなく、入力中に早くフィードバックするための補助機能として扱います。確認後に別の利用者が同じ値を登録できるため、最終判定は登録APIとデータベースの一意制約に残します。この役割分担を前提に、画面ではリクエスト数と古い結果の表示を抑えます。

入力値を正規化して検証する

最初に、APIへ送信できる値か確認します。

userName.ts
export const normalizeUserName = (value: string): string | undefined => {
  const normalized = value.trim().toLowerCase();

  if (!/^[a-z0-9._-]{3,32}$/.test(normalized)) {
    return undefined;
  }

  return normalized;
};

空白を除去して小文字へ変換し、3文字以上32文字以下の英数字と一部の記号だけを受け付けます。

形式が正しくない値はAPIへ送信しません。

デバウンス用のHookを作成する

指定時間だけ値が変わらなかった場合に、新しい値を返すHookを作成します。

useDebouncedValue.ts
import { useEffect, useState } from 'react';

export const useDebouncedValue = <T>(value: T, delayMs: number): T => {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timeoutId = window.setTimeout(() => {
      setDebouncedValue(value);
    }, delayMs);

    return () => window.clearTimeout(timeoutId);
  }, [value, delayMs]);

  return debouncedValue;
};

入力が変わるたびに前のタイマーを解除するため、連続入力中は値が更新されません。

重複チェックAPIを呼び出す

TanStack Queryのenabledを使い、検証済みの値がある場合だけAPIを呼び出します。

useUserNameAvailability.ts
import { useQuery } from '@tanstack/react-query';

type Availability = { available: boolean };

export const useUserNameAvailability = (userName: string | undefined) => {
  return useQuery({
    queryKey: ['userNameAvailability', userName ?? ''],
    enabled: userName !== undefined,
    queryFn: async ({ signal }): Promise<Availability> => {
      if (!userName) throw new Error('userName is required.');

      const response = await fetch(
        `/api/users/availability?userName=${encodeURIComponent(userName)}`,
        { signal },
      );
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return response.json() as Promise<Availability>;
    },
    staleTime: 30_000,
  });
};

入力値をquery keyへ含めることで、ユーザー名ごとに結果を管理できます。

現在の入力と検索結果を対応させる

入力直後は、デバウンス済みの値が1つ前の値を保持しています。

その間に以前の「使用可能」という結果を表示すると、現在入力している値の結果だと誤解されます。

useUserNameValidation.ts
import { useDebouncedValue } from './useDebouncedValue';
import { useUserNameAvailability } from './useUserNameAvailability';
import { normalizeUserName } from './userName';

export const useUserNameValidation = (input: string) => {
  const normalized = normalizeUserName(input);
  const debounced = useDebouncedValue(normalized, 500);
  const query = useUserNameAvailability(debounced);
  const isCurrent = normalized !== undefined && normalized === debounced;

  const available = isCurrent ? query.data?.available : undefined;
  const isChecking = normalized !== undefined && (!isCurrent || query.isFetching);

  return {
    available,
    isChecking,
    isValidFormat: normalized !== undefined,
    canSubmit: normalized !== undefined && available === true,
  };
};

isCurrentがfalseの間は以前の結果を公開せず、「確認中」として扱います。

フォームに表示する

RegistrationForm.tsx
import { useState } from 'react';
import { useUserNameValidation } from './useUserNameValidation';

export const RegistrationForm = () => {
  const [userName, setUserName] = useState('');
  const validation = useUserNameValidation(userName);

  return (
    <form>
      <label htmlFor="userName">ユーザー名</label>
      <input
        id="userName"
        value={userName}
        onChange={(event) => setUserName(event.target.value)}
        aria-describedby="userNameMessage"
      />
      <p id="userNameMessage" aria-live="polite">
        {!validation.isValidFormat && userName && '3〜32文字で入力してください。'}
        {validation.isChecking && '使用できるか確認しています。'}
        {validation.available === true && '使用できます。'}
        {validation.available === false && 'すでに使用されています。'}
      </p>
      <button type="submit" disabled={!validation.canSubmit}>
        登録
      </button>
    </form>
  );
};

重複チェック後に別の利用者が同じ名前を登録する可能性があります。事前チェックは入力を支援する機能です。

登録APIではデータベースの一意制約を使用し、競合した場合はエラーを返します。

確認するポイント

状況期待する結果
形式が不正APIを呼び出さない
連続入力中最後の入力から500ミリ秒待つ
入力値を変更前の結果を表示しない
使用可能登録ボタンを有効にする
使用済みエラーを表示する

デバウンスだけでなく、表示している結果が現在の入力値に対応しているか確認することが大切です。


関連記事

  • React TanStack Queryでバックグラウンド処理をポーリングする

    CSVファイルのインポートなど、完了まで時間がかかる処理をHTTPリクエストの中ですべて実行すると、タイムアウトする可能性があります。このような処理では、開始APIから処理IDを返し、ブラウザから状態...


  • React TanStack Queryで更新後のキャッシュを再取得する

    TanStack Queryでデータを更新しても、取得済みの一覧や詳細は自動では書き換わりません。更新後に古いデータが表示されないよう、関連するQueryを無効化して再取得します。今回は商品一覧と商品...


  • React TanStack Queryで初期データを複数のキャッシュへ分配する

    管理画面の初期表示で、店舗、商品カテゴリ、配送方法など複数のデータが必要になる場合があります。それぞれのAPIを同時に呼び出す代わりに、初期表示用APIでまとめて取得するとリクエスト数を減らせます。今...


  • TypeScript i18nextの動的な翻訳キーを抽出対象に含める

    i18nextでステータスに応じた文言を表示する場合、翻訳キーを動的に切り替えることがあります。ただし、実行時に正しく表示できることと、翻訳キーの抽出ツールが使用箇所を検出できることは別です。今回は、...


  • React useStateで入力フォームを作成する

    Reactでテキストボックスに入力した値を使用するには、useStateで値を保持します。今回は名前とメールアドレスを入力し、送信ボタンを押すと入力内容を表示するフォームを作成します。この例では入力値...


  • React useStateで一覧の追加と削除を行う

    Reactで一覧を表示するときは、配列をuseStateへ保存できます。今回は簡単な買い物リストを作り、項目の追加と削除を行います。配列のpushやspliceで既存のstateを直接変更するのではな...


  • React useRefで入力欄にフォーカスを当てる

    入力フォームを開いた直後や、入力エラーが出たときに、特定の入力欄へフォーカスしたいことがあります。DOMの要素自体を参照するにはuseRefを使います。currentは初回描画中などにnullになり得...


  • React useMemoで重い計算結果を再利用する

    一覧の絞り込みや並べ替えを描画のたびに行うと、データ量や計算内容によっては画面操作が遅くなります。useMemoは、依存する値が変わらない間、以前の計算結果を再利用します。productsまたはque...


  • React useEffectで無限ループが発生するときに確認すること

    ReactのuseEffectを利用したときに無限ループが発生してしまうことがあります。特に注意したいのが、ESLintのreact-hooks/exhaustive-depsで表示された警告をUpd...


  • React useEffectの後片付けでイベント登録を解除する

    画面の幅が変わったときに表示を更新したい場合、ブラウザのresizeイベントを登録できます。登録したままにすると、コンポーネントが不要になったあとも処理が残るため、useEffectの後片付けを用意し...