【React】ブラウザのアイドル中にフォームをバリデーションしてパフォーマンス最適化🚀
こんにちは。ぬこすけです。
入力フォームでユーザーが入力した値をどのタイミングでバリデーションしていますか?🤔
リアルタイミングでバリデーションしたり、あるいは「入力内容を確認」みたいなボタンをタップした直後にバリデーションしているのではないでしょうか?
このようなタイミングでのバリデーションはいたって普通ですが、今回は この他のタイミングでのバリデーションを提案したい と思います。そのタイミングは ブラウザのアイドル中(ひまな時) です。
実装イメージとしては ユーザーの入力したデータをブラウザのアイドル中にバリデーションしておき、「入力内容を確認」みたいなボタンをタップした時にはすでにバリーデーションの結果が使える、という感じです。
こうすることで 「入力内容を確認」みたいなボタンをタップした時の応答が早くなります 。
( Interaction to Next Paint の改善にもつながるでしょう)
ブラウザにはアイドル中に JavaScript を実行できる requestIdleCallback
という Web API があります。
この記事では requestIdleCallback
を良い感じにラップした idle-task
(v3.3.1)というライブラリを使ってブラウザのアイドル中にフォームをバリデーションの実装例 を紹介します。
なお、 React でコードを書きます。
React での実装例
React の hooks を使って、ブラウザのアイドル中にバリデーションを実行するロジックを実装します。
import { useRef, useEffect } from "react";
import { setIdleTask, forceRunIdleTask, cancelIdleTask } from "idle-task";
export default function useValidateWhenIdle(
input: string,
validate: (input: string) => boolean
) {
const idleTaskIdRef = useRef(NaN);
useEffect(() => {
const validateTask = () => validate(input);
idleTaskIdRef.current = setIdleTask(validateTask);
return () => {
cancelIdleTask(idleTaskIdRef.current);
};
}, [input, validate]);
return () => forceRunIdleTask(idleTaskIdRef.current);
}
この hooks を利用する側のコンポーネントは次のようになります。
import "./styles.css";
import useValidateWhenIdle from "./useValidateWhenIdle";
import { useState, ChangeEventHandler, FormEventHandler } from "react";
import { configureIdleTask } from "idle-task";
configureIdleTask({
debug: true
});
const validateUserName = (userName: string): boolean => {
return /^[a-zA-Z]+$/.test(userName);
};
export default function App() {
const [userName, setUserName] = useState("");
const validateUserNameWhenIdle = useValidateWhenIdle(userName, validateUserName);
const handleUserNameChange: ChangeEventHandler<HTMLInputElement> = ({
target
}) => {
setUserName(target.value);
};
const handleSubmit: FormEventHandler<HTMLFormElement> = async (event) => {
event.preventDefault();
const isValid = await validateUserNameWhenIdle();
if (isValid) {
alert(`YourName: ${userName}`);
} else {
alert(`${userName} is invalid`);
}
};
return (
<form onSubmit={handleSubmit}>
<label>
FirstName:
<input type="text" value={userName} onChange={handleUserNameChange} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
まずは useValidateWhenIdle
の hooks を詳しく見てみましょう。
useValidateWhenIdle
は 2 つの引数を受け取ります。
-
input
: バリデーション対象の文字列 -
validate
: バリデーション対象の文字列を受け取ってバリデーションする関数
そして結果はバリデーション結果の Promise
です。
さらに内部の処理まで詳しくみます。
const idleTaskIdRef = useRef(NaN);
useEffect(() => {
const validateTask = () => validate(input);
idleTaskIdRef.current = setIdleTask(validateTask);
return () => {
cancelIdleTask(idleTaskIdRef.current);
};
}, [input, validate]);
idle-task
の setIdleTask
は、引数の関数をブラウザのアイドル中に実行させます。
setIdleTask(validateTask)
で validateTask
というユーザーの入力した値をバリデーションする関数をブラウザのアイドル中に実行させるわけです。
setIdleTask
の戻り値は ID です。
この ID を使ってアイドル中の関数の実行をキャンセルしたり、関数の結果を取得することができます。
ID は useRef
を使って値を管理しておきます。
そして、 useEffect
を使って、ユーザーが入力した値が変更される度にブラウザのアイドル中にバリデーションが実行されるようにします。
useEffect
のクリーンナップ関数では cancelIdleTask
と使い、まだ対象の処理が実行されていない場合はキャンセルします。
return () => forceRunIdleTask(idleTaskIdRef.current);
setIdleTask
で登録した関数の結果は forceRunIdleTask
で取得できます。
もし対象の関数がすでにブラウザのアイドル中に実行されていたらその結果を返し、まだ実行されていなかった場合は即時実行します。
続いて useValidateWhenIdle
の hooks を使う側のコンポーネントを詳しく見てみましょう。
import "./styles.css";
import useValidateWhenIdle from "./useValidateWhenIdle";
import { useState, ChangeEventHandler, FormEventHandler } from "react";
import { configureIdleTask } from "idle-task";
configureIdleTask({
debug: true
});
const validateUserName = (userName: string): boolean => {
return /^[a-zA-Z]+$/.test(userName);
};
export default function App() {
const [userName, setUserName] = useState("");
const validateUserNameWhenIdle = useValidateWhenIdle(userName, validateUserName);
const handleUserNameChange: ChangeEventHandler<HTMLInputElement> = ({
target
}) => {
setUserName(target.value);
};
const handleSubmit: FormEventHandler<HTMLFormElement> = async (event) => {
event.preventDefault();
const isValid = await validateUserNameWhenIdle();
if (isValid) {
alert(`YourName: ${userName}`);
} else {
alert(`${userName} is invalid`);
}
};
return (
<form onSubmit={handleSubmit}>
<label>
FirstName:
<input type="text" value={userName} onChange={handleUserNameChange} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
React の基本的なフォームの実装例です。
useState
を使ってユーザーに入力させたいデータを状態管理します。
handleUserNameChange
というユーザーが入力した時に発火する関数を用意し、 input
タグに紐づけます。
そして Submit ボタンが押されたら handleSubmit
が実行されます。
さて、ここで注目すべきポイントを取り上げます。
const validateUserNameWhenIdle = useValidateWhenIdle(userName, validateUserName);
先ほど用意した useValidateWhenIdle
が出てきました。
第一引数にバリデーションの対象となる文字列、第二引数にバリデーションのロジックを定義した関数が指定されています。
そして戻り値には関数が返ってきています。
バリーデーション結果の Promise
は次のように Submit ボタンが押したときに使います。
const handleSubmit: FormEventHandler<HTMLFormElement> = async (event) => {
event.preventDefault();
const isValid = await validateUserNameWhenIdle();
if (isValid) {
alert(`YourName: ${userName}`);
} else {
alert(`${userName} is invalid`);
}
};
Submit ボタンが押したときに async/await
を使ってバリデーション結果を取得しています。
その結果を元に alert
を出しているわけです。
今まで紹介したコードで実装したページは次の URL で確認できます。
実際にブラウザのアイドル中にバリデーションが行われているか見てみましょう!
次のコードを追加することで、バリデーション関数の実施結果がブラウザのコンソールに出力されます。
import { configureIdleTask } from "idle-task";
configureIdleTask({
debug: true
});
先ほど紹介したページで入力エリアにテキトーな文字を入れると、バリデーションがアイドル中に行われていることがわかります。
(数値は丸められるので 0 ms のようになっています)
また Chrome ディベロッパーツールの Performance タブから計測すると、ブラウザのアイドル中に関数が動いていることがわかります。
このようにバリデーションがブラウザのアイドル中に行われている様子がわかりました。
今までで紹介したコード例は CodeSandbox に載せています。
まとめ
リアルタイミングでバリデーション、あるいは「入力内容を確認」みたいなボタンをタップした直後にバリデーションする以外にも、ブラウザのアイドル中にバリデーションをかましておく方法をご紹介しました。
今回紹介した idle-task
というライブラリでもしバグや質問などあれば Github の issue なり この記事でコメントいただければと思います。日本語で OK です!
ついでに Github Star ⭐をくれると嬉しいです!
よりフロントエンドのパフォーマンス最適化に興味がある方はこちらの記事もぜひ参考にしてみてください。
ここまでご覧いただきありがとうございました!by ぬこすけ
Discussion