背景
Next.js + TypeScript な環境で react-hook-form と urql を使ってユーザー登録フォームを実装していた時に、ユーザー名が入力されると同時にリアルタイムでそのユーザー名が既に使われているかどうか調べる必要があった。
urql の useXxxQuery
は、何もオプションを渡さなければコンポーネントのマウント時に必ずクエリを実行してしまう。
しかしユーザーがユーザー名を入力した後に、ユーザーの入力値が変わるたびにクエリを実行して一意性を確かめたかったので、やり方を調べた時の備忘録として書いておく。
環境
- Next.js v12.2.1
- React Hook Form v7.33.1
- urql v2.2.2
- @graphql-codegen/cli v2.8.0
- @graphql-codegen/typescript-urql v3.6.1
前提
- graphql-codegen の typescript-urql プラグインで、必要なクエリの型と hooks を自動生成している。
- 今回は下記のようなシンプルなクエリを実行する。
query getAvailableUsername($username: String!) {
availableUsername(username: $username)
}
すると graphql-codegen によって下記のような型と hooks が自動生成される。
export const GetAvailableUsernameDocument = gql`
query getAvailableUsername($username: String!) {
availableUsername(username: $username)
}
`;
export function useGetAvailableUsernameQuery(options: Omit<Urql.UseQueryArgs<GetAvailableUsernameQueryVariables>, 'query'>) {
return Urql.useQuery<GetAvailableUsernameQuery>({ query: GetAvailableUsernameDocument, ...options });
};
フォームを用意する
まずは urql を考えず、シンプルにユーザー名のみを入力できるフォームを実装してみる。
interface Props {
onSubmit: () => void;
}
const RegistrationForm: React.FC = ({ onSubmit }) => {
const {
register,
handleSubmit,
} = useForm<FormValues>();
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input type="text" {...register('username')} placeholder="ユーザー名を入力" />
<button>登録</button>
</form>
);
};
export default RegistrationForm;
これでユーザー名の入力欄と登録ボタンのみが表示されるシンプルなフォームを実装できた。
次に、このフォームにユーザー名を入力した時に、リアルタイムでサーバーに問い合わせる方法を考えよう。
結論から言うと、公式ドキュメントの Pausing という章に答えが書いてあった。
https://formidable.com/open-source/urql/docs/basics/react-preact/#pausing-usequery
pause
というオプションを用いて、条件が揃った時のみ(= pause: true の時のみ)サーバーにリクエストを送信することができる、ということだ。
interface Props {
onSubmit: () => void;
}
const RegistrationForm: React.FC = ({ onSubmit }) => {
const {
watch, // 追加
register,
handleSubmit,
} = useForm<FormValues>();
const username = watch('username') ?? ''; // 入力中のユーザー名の値を監視する
// username が存在する時のみ getAvailableUsernameQuery を実行する
const [{ fetching, data }] = useGetAvailableUsernameQuery({
variables: { username },
pause: !username,
});
// サーバーに問い合わせ中の状態
const isCheckingUsername = fetching && !!username;
// 入力されたユーザー名が既に使われている状態
const usernameIsUsed = data && !data.availableUsername;
// 入力されたユーザー名が誰にも使われていない状態
const usernameIsAvailable = !isCheckingUsername && !!username && !usernameIsUsed;
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input type="text" {...register('username')} placeholder="ユーザー名を入力" />
{isCheckingUsername && <p>確認中...</p>}
{usernameIsUsed && <p>このユーザー名は既に利用されています。</p>}
{usernameIsAvailable && <p>このユーザー名は利用可能です。</p>}
<button>登録</button>
</form>
);
};
export default RegistrationForm;
ポイント
- react-hook-form の watch を利用して、ユーザー入力中の値を取得する。
- urql の pause オプションを利用して、 username が入力されるまでクエリの実行を回避する。
クエリ結果のキャッシュについて
- ユーザー入力の度にサーバーにリクエストを送信すると負荷が高くなる懸念があるが、 urql を利用することでその懸念を多少は減らすことができる。
- urql は、同じ __typename に対して同じ argument が渡された場合、デフォルトではサーバーに問い合わせず、前回リクエスト時にキャッシュしておいた結果を返す。
- ユーザーがユーザー名を何度も入力し直した場合、1度でも確認済みの文字列に対してはキャッシュを参照するため、サーバーへのリクエスト回数を減らすことができる。
- 一方で、ユーザーが何度もユーザー名を入力し直しているうちに、そのユーザー名を利用していた別のユーザーが退会したり、逆に利用できると表示されたユーザー名が他のユーザーに取られてしまうといった事故も考えられる。
- しかし頻度がかなり稀であると予想されることと、仮にユーザー名を他のユーザーに取られてしまったとしても、mutation 実行時のエラーメッセージを表示することでユーザーに何が起きたか説明することができることから、大きな問題にはならないと思われる。
改善の余地
今回は react-hook-form の watch を用いてユーザーが入力中の値を取得したが、この場合、1文字入力するごとにレンダリングやネットワーク通信が発生してしまう。
react-hook-form のオプションか何かを用いて、例えば blur 時に入力値の変更と見なしてレンダリングやネットワーク通信を発生させるようなやり方があると、より良くなりそう。
結論
urql の useXxxQuery は、コンポーネントのマウント時には実行せず、条件が満たされた時のみ実行するということができる。