TanStack Queryの useQuery
のドキュメントを見ていたところ、 select
オプションがありました。
This option can be used to transform or select a part of the data returned by the query function. It affects the returned data value, but does not affect what gets stored in the query cache.
select
オプションの挙動について調べたところ、以下のBlogに記載がありました。
React Query のレンダリング最適化を目指した話 - Techtouch Developers Blog
そこで、上記のBlogを参考にしつつ、気になったところを調べてみたときのメモを残します。
目次
- 環境
- selectを使ったデータの絞り込み
- selectで絞り込むと再レンダリングされない
- selectで絞り込んでも再レンダリングされるケース
- 参考資料:TanStack Queryのデータ変換やレンダリング最適化について
- ソースコード
環境
- React 18.2.0
- TanStack Query 4.32.6
環境は以前の記事と同じため、詳しくはそちらを参照してください。
Reactにて、useStateやuseEffectを使っていたところをTanStack Queryに置き換えてみた - メモ的な思考的な
selectを使ったデータの絞り込み
useQuery
でAPIから取得したデータを絞り込む場合、 useQueryの
queryFn
に定義した関数のthen
の中で絞り込むselect
に定義した関数にて絞り込む
の2つの方法が考えられました。
そこで、今回「バックエンドから取得したデータのうち、最初のデータに絞り込む」という実装を、各パターンでためしてみます。
まずは queryFn
で絞り込む場合です。このときは then
の中で絞り込みを行います。
// thenの中で絞り込みを行う const queryWithFilter = () => new DefaultApi().fetchFishes({headers: {Prefer: 'dynamic=true'}}).then((response) => response.data.fishes[0]) const Page = () => { // useQueryでは絞り込みを行わない const {data: queryFnData} = useQuery({ queryKey: ['DataFilter', 'ByQueryFn'], queryFn: queryWithFilter, }) // ... }
一方、 select
で絞り込む場合は、 queryFn
の関数では特に絞り込みを行わず、 select
で指定した関数の方で絞り込みます。
// 絞り込みを行わない const queryWithoutFilter = () => new DefaultApi().fetchFishes({headers: {Prefer: 'dynamic=true'}}).then((response) => response.data.fishes) const Page = () => { // const {data: selectData} = useQuery({ queryKey: ['DataFilter', 'BySelect'], queryFn: queryWithoutFilter, select: (fishes) => { return fishes[0] }, }) // ... }
では、実際にコンポーネントへ組み込み、動作確認してみます
import {useQuery} from "@tanstack/react-query"; import {DefaultApi} from "../../../../../types"; const queryWithFilter = () => new DefaultApi().fetchFishes({headers: {Prefer: 'dynamic=true'}}).then((response) => response.data.fishes[0]) const queryWithoutFilter = () => new DefaultApi().fetchFishes({headers: {Prefer: 'dynamic=true'}}).then((response) => response.data.fishes) const Page = () => { const {data: queryFnData} = useQuery({ queryKey: ['DataFilter', 'ByQueryFn'], queryFn: queryWithFilter, }) const {data: selectData} = useQuery({ queryKey: ['DataFilter', 'BySelect'], queryFn: queryWithoutFilter, select: (fishes) => { return fishes[0] }, }) if (!queryFnData || !selectData) return <>No data...</> return ( <> <h1>結果(queryFnでフィルター)</h1> <p>ID: {queryFnData.id} / Name: {queryFnData.name}</p> <hr /> <h1>結果(selectでフィルター)</h1> <p>ID: {selectData.id} / Name: {selectData.name}</p> </> ) } export default Page
すると、両方とも、最初のデータだけの絞り込みができました。
selectで絞り込むと再レンダリングされない
queryFn
と select
のどちらでも絞り込むことができました。
次に、それらの違いは何かを見てみます。
冒頭の記事によると、
React Query はデフォルトだと取得データを比較しますが、select オプションが設定されている場合は select の戻り値を比較するようになります。そのため取得データに変更があったとしても、必要なデータに変更がなければ再レンダリングは発生しなくなります。
とあったため、ためしてみます。
準備: React Dev Toolsの Highlight updates when components render をON
今回、再レンダリングされることを確認します。
そこで、React Dev Toolsの Components
タブにある Highlight updates when components render.
にチェックを入れて、再レンダリングの発生をわかりやすくしておきます。
準備:PrismでArrayのレスポンス件数を固定化する
今回はPrismでFakeデータを返しているため、何も設定しないとAPIからレスポンスされるデータ件数が変化してしまいます。
その結果、再レンダリングの発生が
- データ件数が変化したため
- selectを使っていないため
のどちら由来かが分かりづらいです。
そこで、OpenAPIスキーマにPrism用の設定を追加し、レスポンスデータの件数を固定化します。
Prisimのドキュメントによると、OpenAPIスキーマの冒頭に x-json-schema-faker
を定義すれば良さそうでした。
Dynamic Response Generation with Faker | Prism
そこで、今回はレスポンスの件数を3件で固定化するよう、設定を追加しました。
openapi: 3.0.0 x-json-schema-faker: min-items: 3 max-items: 3
なお、この設定はグローバルなため、全APIのレスポンスへ影響してしまいます。
ただ、今回の検証するにあたっては問題にはならないので、気にせず進めます。
selectオプションなしの場合
最初に、 queryFn
にAPIを呼ぶ関数を定義しておきます。
という実装にします。
import {useQuery} from "@tanstack/react-query"; import {DefaultApi} from "../../../../../../types"; const queryFn = () => new DefaultApi().fetchFishes({headers: {Prefer: 'dynamic=true'}}).then((response) => response.data.fishes) const Page = () => { const {data, refetch} = useQuery({ queryKey: ['DataFilter'], queryFn: queryFn, }) const handleClick = () => { refetch() } return ( <> <h1>結果(queryFnの結果をフィルター)</h1> <p>{data?.length}件あります</p> <hr /> <button type={"button"} onClick={handleClick}>リロード</button> </> ) } export default Page
実装ができたので、動作を確認します。
すると、ボタンを押したタイミングで再レンダリングが発生しました。
selectオプションありの場合
続いて、 select
オプションありの場合です。
違いは useQuery
の引数だけです。 select
オプションの中で件数を算出してしまいます。
今回の場合、APIのレスポンス件数は3件で固定なため、この設定だと再レンダリングは発生しない想定です。
const {data, refetch} = useQuery({ queryKey: ['DataFilter'], queryFn: queryFn, select: (fishes) => { return fishes.length }, })
動作を確認してみると、何度ボタンをクリックしても再レンダリングは発生しませんでした。
selectで絞り込んでも再レンダリングされるケース
select
オプションを指定することで再レンダリングを防げました。
ただ、 useQuery
では、参照しているプロパティに変更がある場合、再レンダリングが発生します。
そのため、どうしても再レンダリングを防止するには、参照しているプロパティのことも考える必要があります。
そこで今回は
を行ってみます。
dataとisRefetchingを参照し、selectで絞り込むだけでは再レンダリングが発生する
useQuery
には数多くのプロパティがあります。
https://tanstack.com/query/v4/docs/react/reference/useQuery
今回は、 data
の他に isRefetching
への参照を追加し、再レンダリングが発生する様子を見ていきます。
そのための差分は、
const {data, refetch, isRefetching} = useQuery({})
で、isRefetching
の参照を追加する<p>isRefetching: {isRefetching.toString()}</p>
を追加し、isRefetching
の値を確認できるようにする
です。
また、ソースコード全体は以下です。
import {useQuery} from "@tanstack/react-query"; import {DefaultApi} from "../../../../../../types"; const queryFn = () => new DefaultApi().fetchFishes({headers: {Prefer: 'dynamic=true'}}).then((response) => response.data.fishes) const Page = () => { const {data, refetch, isRefetching} = useQuery({ queryKey: ['DataFilter'], queryFn: queryFn, select: (fishes) => { return fishes.length }, }) const handleClick = () => { refetch() } return ( <> <h1>結果(selectでフィルター、isRefetchingあり)</h1> <p>isRefetching: {isRefetching.toString()}</p> <p>{data}件あります</p> <hr /> <button type={"button"} onClick={handleClick}>リロード</button> </> ) } export default Page
動作確認すると、 select
オプションがあるにも関わらず、再レンダリングが発生しています。
また、動画では分かりづらいかもしれませんが、 isRefetching
の表示にも動きがあります。
notifyOnChangePropsを使い、dataの変更だけ再レンダリングする
useQuery
のオプションを見たところ、 notifyOnChangeProps
がありました。
If set, the component will only re-render if any of the listed properties change. If set to ['data', 'error'] for example, the component will only re-render when the data or error properties change. If set to "all", the component will opt-out of smart tracking and re-render whenever a query is updated. If set to a function, the function will be executed to compute the list of properties. By default, access to properties will be tracked, and the component will only re-render when one of the tracked properties change.
そこで、今回 notifyOnChangeProps
に data
を追加し、dataに変更があるときのみ再レンダリングするよう修正します。
前との差分は、useQuery
のオプションのみです。
const {data, refetch, isRefetching} = useQuery({ queryKey: ['DataFilter'], queryFn: queryFn, select: (fishes) => { return fishes.length }, notifyOnChangeProps: ['data'] // 追加 })
動作確認すると、 data
は select
により再レンダリングされないことから、全体の再レンダリングも発生しなくなりました。
参考:v4系では、notifyOnChangePropsに tracked を設定できない
冒頭のBlogにあった tracked
という指定は、現在はできなくなっています。
https://github.com/TanStack/query/releases/tag/v4.0.0-alpha.1
その代わり、参照していないプロパティは再レンダリングの対象外となっているようです。
参考資料:TanStack Queryのデータ変換やレンダリング最適化について
TanStack Queryの著者のBlogに詳細な記事がありました。
今回の記事でためしていないことなども書かれていて、とても参考になりました。
ソースコード
Githubに上げました。
https://github.com/thinkAmi-sandbox/tanstack_prism_generouted-example
今回のプルリクはこちら。
https://github.com/thinkAmi-sandbox/tanstack_prism_generouted-example/pull/3