Reactで検索フォームを作る場合の基本的なフロー
Reactで検索フォームを作る場合の基本的なフローを紹介。
利用するツールは下記のとおり。SPAでのフローを想定している。
- React
- React Router V6
- TanStack Query
- Vite
こういう検索フォーム
- フリーワードで検索ができたり、更新日時のfrom / to を選択できたり、複数の条件を選択できる。
- 検索条件をURLパラメータに保存。URLの状態からフォームの状態を作ることができる。
- 「検索ボタン」をタップすると検索が実行される。
フロー概要
URLパラメータを取得
まずはURLパラメータを取得する。URLパラメータとはURLの?
以降に渡される文字列。
React Router を利用すると、以下のようなHooksで入手できる。
const [searchParams, setSearchParams] = useSearchParams();
この searchParams
を useEffect
でリッスンする。
useEffect(() => {
...
}, [searchParams]);
こうすることでURLパラメータが変更すると、このuseEffect内の処理を実行することができる。ページが最初に読み込まれたタイミングと、検索ボタンをタップした場合にのみこのEffectが発火する想定。
検索を実行するmutationの作成
formStateをAPI用のパラメータへの変換はここで行う。直前で行うことにより、UI側はAPIパラメータの型について特に気にする必要はなくなる。変換方法は後述する。
そして、useQueryではなくuseMutationを利用するのがポイント。GETメソッドなのでuseQueryを使うのかな?と脳死で思い込んでしまっていた面もあって、非常に面倒なフローになってしまっていた。「検索ボタンをタップして検索を実行」なので、useQueryではなく、明示的なアクションがあって実行できる useMutation のほうが用途として正しい。
もう一つのポイントはformStateを引数として受け取っているところ。useStateの値としても利用できるが、useStateした直後にmutationを実行したい場合、mutationが受け取るパラメータは前回のステートのものになってしまう。引数で渡せば最新のステートを利用することができる。
フォームコンポーネントに最新の状態を渡しつつ、同時にAPIも叩きたい、という今回の用途には引数で受け取った方がよい。
httpはAxiosのインスタンスだが、この記事とは関係が無いことなので省略する。
const mutation = useMutation(
(formState) => {
const params = createRequestParams(formState);
if (!params) {
return Promise.reject();
}
return http.get("/hoge/fuga/", {
params,
});
},
{
onSuccess: (data) => {
setApiResponse(data.data);
},
onError: (error) => {
console.error("error", error);
},
},
)
フォームのステートに変換する
URLパラメータはURLSearchParamsというオブジェクトとして利用ができるが、このままではReactのコンポーネントで扱えないので(扱えるかもしれないが初期値を設定したい)、単純なオブジェクトに変換する。
useEffect(() => {
const search = searchParams.get("search") ?? "";
const startDateFrom = searchParams.get("startDateFrom") ?? "";
const startDateTo = searchParams.get("startDateTo") ?? "";
const newFormState = { search, startDateFrom, startDateTo }
mutation.mutate(newFormState);
setFormState(newFormState)
}, [searchParams]);
mutation.mutate
でAPIをコールしつつ、setFormStateでフォームコンポーネントにステートを渡している。
このエフェクトはページを読み込んだ直後と、送信ボタンをタップした直後に発火する。もしかしたら、前回のsearchParamsと今回のsearchParamsで比較検証して、異なっていたら実行のほうが良いかもしれない。が、今回は無駄なレンダリングも発生していないのでこのままでいいだろう。
APIの送信用パラメータに変換する
mutation内でcreateRequestParamsを使ってフォームステートをAPI送信パラメータに変換している。以下サンプル。
const createRequestParams = (formState: SearchFormState) => {
const requestParams: ContentRequestParams = {
pageSize: INITIAL_PAGE_SIZE,
p: formState.currentPage ?? 1,
};
const _errors: FormError[] = [];
if (formState.search) {
requestParams.search = formState.search;
}
if (formState.startDateFrom && formState.startDateTo) {
const startDateFromMs = new Date(formState.startDateFrom).getTime();
const startDateToMs = new Date(formState.startDateTo).getTime();
if (startDateFromMs > startDateToMs) {
_errors.push({
key: "startDateTo",
message: "ToはFromより後の日付を入力してください",
});
}
...
if (_errors.length > 0) {
setErrors(_errors);
return undefined;
}
return requestParams
}
値のバリデーションもここで行っている。エラーがあったらundefinedを返すので、mutation内ではrejectををリターンすることになる。
フォームの入力
<SearchForm
formState={formState}
formErrors={formErrors}
onChangeField={(key: keyof SearchFormState, value: unknown) => {
onClearFormErrors();
setFormState((prev) => ({ ...prev, [key]: value }));
}}
onReset={() => {
onClearFormErrors();
setFormState(defaultFormState);
}}
onSubmit={() => {
onSearch();
}}
/>
単純にフォームステートに入力した値を入れているだけ。API型への変換はmutation内で行うので、ここでは気にする必要がない。バリデーションが複雑ではないフォームの場合は、React Hook Formなどを利用せずともstateの利用で十分だと考える。ミニマムで済ませられるならミニマムで済ませたい派。
送信ボタンをクリック
const onUpdateUrl = (formState: SearchFormState) => {
const params = cleanedParams(formState);
setSearchParams(params as unknown as URLSearchParams);
};
const onSearch = () => {
const newFormState = { ...formState, currentPage: 1 };
setFormState(newFormState);
onUpdateUrl(newFormState);
}
送信ボタンはAPIのコールするのではなく、URLパラメータを変化させるだけ。URLが変更すれば、useEffectが発火して、APIをコールしてくれるからだ。
入力 → 送信 → URLの更新 → エフェクトの発火 → APIをコール & フォームの状態を変更、というフローだ。
最後のフォームの状態の変更は、初回以外は必要ない。初回はURLからフォームの状態を作る必要があるので必須。
まとめ
おおよその検索フォームはこのフローで実現できるのではないかと思われる。
入力すると同時にAPIをコールするリッチな検索は、もっと仕様を考える必要があるのでご注意を。
Discussion