React 入力値の重複チェックをデバウンスして実行する
ユーザー名の登録フォームでは、入力した値がすでに使用されているかAPIで確認することがあります。
入力のたびにAPIを呼び出すとリクエストが増えるため、入力が止まってから重複チェックを実行します。
今回は形式チェック、デバウンス、APIによる重複チェックを組み合わせる方法を紹介します。
入力値を正規化して検証する
最初に、APIへ送信できる値か確認します。
userName.tsexport 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.tsimport { 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.tsimport { 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.tsimport { 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.tsximport { 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ミリ秒待つ |
| 入力値を変更 | 前の結果を表示しない |
| 使用可能 | 登録ボタンを有効にする |
| 使用済み | エラーを表示する |
デバウンスだけでなく、表示している結果が現在の入力値に対応しているか確認することが大切です。