useDeferredValue を使ってUXを向上
はじめに
今回、Next.js
(v14 AppRouter)でのフォーム制作の話になりますがReact
での制作にも適用できると思います。
フォーム入力に応じてリストにフィルターをかける実装をした時に、入力文字の削除にタイムラグを感じる場面がありました。
具体的には、入力した文字列を一字ずつ削除していった際に入力フォーム欄の文字列が微妙に遅れて削除(または2文字一挙に削除)されるといった状況です。
近似体験としては、ネット環境が悪い状況で重たいアプリ(例:Adobe Illustrator, Photoshop, Indesign など)を開いて、文字入力・削除した際のラグってる感じでしょうか。
正直UXが良くないので解消するために今回useDeferredValue
を使用しました。
実装もシンプルで簡単でしたし、このフックを使うと無事に解消できたので情報共有したいと思います。
フォーム入力に応じてリストをフィルター
今回解消したかった当該コード(フォーム入力に応じてリストにフィルターをかける実装)は以下になります。
"use client"
import { ChangeEvent, memo, useEffect, useState } from "react";
import { useAtom } from "jotai";
function ViewLists() {
const [listsItems, setListsItems] = useAtom(listsItemsAtom);
const [keyword, setKeyword] = useState<string>(keyword ?? '');
const [lists, setLists] = useState<listsItemsType[]>([]);
return (
<>
<label>
<span>キーワードを入力してください。</span>
<input type="text" value={keyword}
onInput={(e: ChangeEvent<HTMLInputElement>) =>
filterData(
e.target.value,
listsItems,
setKeyword,
setLists
)} />
</label>
<FilterLists props={{
keyword: keyword,
lists: lists,
listsItems: listsItems
}} />
</>
);
}
export default memo(ViewLists);
input
のonInput
イベントハンドラーでfilterData
を実行し、入力文字に基づいたリストを生成(フィルター)しています。
そしてフィルターされたlists
の中身はFilterLists
コンポーネントにて表示される実装です。
この実装だと挙動は問題ないのですが冒頭で説明した通り、入力時(削除時)に入力欄の文字(input
のvalue
)更新にタイムラグが生じました。
useDeferredValue
を使用した改良コード
- import { ChangeEvent, memo, useEffect, useState } from "react";
+ import { ChangeEvent, memo, useDeferredValue, useEffect, useMemo, useState } from "react";
function ViewLists() {
const [listsItems, setListsItems] = useAtom(listsItemsAtom);
const [keyword, setKeyword] = useState<string>(keyword ?? '');
const [lists, setLists] = useState<listsItemsType[]>([]);
+ const deferedLists: listsItemsType[] = useDeferredValue(lists);
+ const deferedFilterLists = useMemo(
+ () => <FilterLists props={{
+ keyword: keyword,
+ lists: deferedLists,
+ listsItems: listsItems
+ }} />,
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [deferedLists, keyword]
+ );
return (
<>
<label>
<span>キーワードを入力してください。</span>
// 入力欄部分は省略
</label>
- <FilterLists props={{
- keyword: keyword,
- lists: lists,
- listsItems: listsItems
- }} />
+ {deferedFilterLists}
</>
);
}
export default memo(ViewLists);
あらかじめ任意のstate
更新における緊急性をマークし、緊急性の低い更新を必要に応じて遅らせることができるのがuseDeferredValue
でしたね。
今回、緊急性の低い更新に該当するstate
はlists
です。
以下のようにuseDeferredValue
にlists
を指定します。
const deferedLists: listsItemsType[] = useDeferredValue(lists);
deferedLists
を使用するには適用するコンポーネントをメモ化して依存配列に遅延する値を入れる必要があります。
今回、適用するコンポーネントはFilterLists
で、それをメモ化して依存配列に遅延する値はdeferedLists
です。
※入力に応じた処理のためkeyword
も依存配列に指定しています。
const deferedFilterLists = useMemo(
() => <FilterLists props={{
keyword: keyword,
lists: deferedLists,
listsItems: listsItems
}} />,
// eslint-disable-next-line react-hooks/exhaustive-deps
[deferedLists, keyword]
);
この実装によって、入力欄の文字(input
のvalue
)を更新するためのレンダリングが優先されてリスト情報(lists
)の更新のレンダリングが遅延されるようになりました。
最後にJSX
内にdeferedFilterLists
を記述して反映させれば完了です。
- <FilterLists props={{
- keyword: keyword,
- lists: lists,
- listsItems: listsItems
- }} />
+ {deferedFilterLists}
これで入力の度に発生していた微妙なタイムラグは無事に解消されました!
まとめ
フォーム入力による操作でタイムラグを感じるときはuseDeferredValue
を検討してみてください。
何か間違いや他の良い方法をご存知の方は、お手数ですがコメント欄などで指摘いただけますと幸いです。
ここまで読んでいただき、ありがとうございました。
参考情報
Discussion