背景: ユーザーアクションを通してfetchをしたい
データをサーバー間でやり取りする際,非同期な状態管理ライブラリとしてTanstack Query(旧react-query)が選択されることは少なくありません.
(ちなみに,自分は調べてから知ったのですが,Tanstack Queryの利点はあくまでデータのフェッチや更新を非同期に行える,つまりisLoading,onSuccessなど,非同期処理が楽になった,という点です.データフェッチのためのライブラリ,という勘違いをしていました.恥ずかしい)
Tanstack queryでデータをフェッチする際,取得のタイミングは2パターンあります.
1つ目は,画面の初回表示でフェッチする,2つ目は,ユーザーのアクションによってフェッチするときです.
初回表示でデータ取得をするのは,公式にもあるように特に問題はないでしょう.
一方で,2つ目のユーザーのアクションをトリガーとしてデータ取得をしたい場合があります.
例えば,ボタンを押したらuseQuery()が実行されるようにしたい.などです.
Tanstack Queryには,何らかのイベントによって実行するhooksはありません.
(Apollo ClientのuseLazyQuery()的なものがあればいいんですけどね)
そこで,useQueryのオプションで enabled: false
を指定し,refetch()
を実行することで実現します.
const { data, isPending, refetch } = useQuery({
queryKey: ["example"],
queryFn: () => axios.get("/example").then(res => res.data)
enabled: false //これを指定する
})
...
<button type="button" onClick={refetch}>リフェッチ</input>
問題点: refetch()には引数が割り当てられない
基本的には,上記の方法でイベントによるrefetch
の実行は可能ですが,問題なのは引数が渡せないことです.
例えば,リストをクリックしたらそのリストの詳細内容をフェッチしたいといった場合には,叩くAPIにはそのリストの情報(よくあるのはid)を含めなければいけません.
refetch()
はそもそもuseQueryのqueryFn
を再実行することを目的としているので,「そこで設定された引数があるからrefetch関数でわざわざ引数を入れる必要はないよね?」というスタンスなんだと思います
解決策
optionのqueryKeyにstateを含める
@honey32 さん,ありがとうございます.
もともとはuseEffectを使う,といった方法を記載していましたが,その必要はなさそうでした.大変失礼いたしました.
queryKeyというのは,クエリのキャッシュを管理するための一意なキーのことです.
stringか,情報を追加する場合にはarrayで指定できます.
しかし,キャッシュ管理のための情報が不十分なときだけではなく, queryFnの引数(queryFnを実行する際に変更されてしまう変数)もqueryKeyの配列に含める必要があるそうです.
公式にもばっちり書いてありました...
こちらは,公式の引用です.
Note that query keys act as dependencies for your query functions. Adding dependent variables to your query key will ensure that queries are cached independently, and that any time a variable changes, queries will be refetched automatically (depending on your staleTime settings). See the exhaustive-deps section for more information and examples.
つまり,引数が変更されるたびにrefetchされるため,refetch()
にもこの仕組みを利用します.
詳しくはこちら
https://tanstack.com/query/latest/docs/framework/react/guides/query-keys#if-your-query-function-depends-on-a-variable-include-it-in-your-query-key
以下,いただいたコメントの引用です...
queryKey オプションが、このような再取得のニーズのために用意されています。state を 「依存する値」として queryKey に含めるだけでOKです。
「初期描画では取得しない」という挙動は、 「state ステートの初期値」をきちんと決めておくことで解決できます。
queryKey オプションが、このような再取得のニーズのために用意されています。state を 「依存する値」として queryKey に含めるだけでOKです。
「初期描画では取得しない」という挙動は、 「state ステートの初期値」をきちんと決めておくことで解決できます。
const [state, setState] = useState(undefined);
const { data, isPending } = useQuery({
queryKey: ["example2", state],
queryFn: (variables) =>
axios.get("/example2", variables).then(res => res.data)
enabled: state === undefined // 初期状態では、取得しないでおく
})
ここから先は更新前の情報です.参考のために残してありますが,解決策は,↑の方が最適です.ご注意ください.
useEffectを使う
直前にも書きましたが,@honey32さんに指摘していただいた通り,useEffectをわざわざ使う必要はありませんので,解決策だけ知りたい方はqueryKey
const useExampleQuery = (variables) => {
return useQuery({
queryKey: ["example2"],
queryFn: (variables) => axios.get("/example2", variables).then(res => res.data)
enabled: false
})
}
const exampleData = [
{id: 1, name: "佐代子"},
{id: 2, name: "布津子"},
{id: 3, name: "美代子"},
]
const Example = () => {
const [state, setState] = useState()
const { data, isPending } =useExampleQuery(state)
useEffect(() => {
if(state) {
refetch()
}
},[state, refetch])
return (
<>
{exampleData.map({id, name} => (
<div key={id} onClick={() => setState(id)}>{name}</div>
))}
</>
)
}
少しだけ補足
stateを引数として使うことが分かりやすいように,カスタムフックを使ってuseQueryを分割しました.
ポイントは,useEffectを使うことです.
わざわざuseEffectを使わずとも,onClickイベントの中にsetState(id)
とrefetch()
を一緒に含めればいいじゃないかと思われるかもしれませんが,それだとstateの値が更新されないままrefetchが実行されてしまいます.
これは,setStateによりstateが更新され,レンダリングされた後でないと,refetch(state)の引数にあるstateも更新されないからです.
つまり,onClickイベントの中にsetState(id)
とrefetch()
を一緒に含めてしまうと,refetch()はレンダリングされる前に実行されます.
そのため,再レンダリングの副作用として実行される,useEffectを使うことでうまく引数が更新される仕組みです.
パフォーマンスを重視する場合はuseCallback
を使うのが良いと思います.