3行でまとめると
1:tanstack よく分からなかったから、自作してみたよ
2:querykey・retry など基本機能は作ったけど、staleTime・invalidate などは作っていないよ
3:tanstack のデータ構造は意外と簡単。
成果物
趣味の開発で tanstack の存在を知ったが、よく正体が分からなかったので、簡単な模倣物を自作してみました。
動くものは作れたのですが、細かい部分で現物と違っている可能性もあるので、間違っていたら指摘くださると嬉しいです。
参考にしたコードは
使い方
https://github.com/YmBIgo/my-tanstack-query/tree/main のソースコードの src/my-tanstack
配下を持ってきて、以下のように使います(なんか v4 みたいな書き方になってしまった、、、)。
import * as React from "react";
import {useQuery} from "./my-tanstack/hooks/useQuery"
import axios from "axios";
export const App = () => {
const [todoId, setTodoId] = React.useState<number>(1);
+ const {result} = useQuery(`hoge${todoId}`, () => {
+ return axios.get(`https://jsonplaceholder.typicode.com/todos/${todoId}`).then(res => res.data);
+ }, 3, 3000);
const onClickIncrement = async() => {
setTodoId(todoId+1);
}
const onClickDecrement = async() => {
if (todoId === 1) return;
setTodoId(todoId-1);
}
if (result?.isLoading) {
return (
<p>Loading...</p>
)
}
if (result?.isError) {
return (
<p>Error</p>
)
}
return(
<div>
Hello Qiita!!
<p>user id : {result?.data?.userId}</p>
<p>title : {result?.data?.title}</p>
<p>id : {result?.data?.id}</p>
<button onClick={onClickIncrement}>increment</button>
<button onClick={onClickDecrement}>decrement</button>
</div>
);
};
useQuery の引数はそれぞれ以下の通りです。
第一引数:キー
第二引数:axios や fetch などデータ取得する関数
第三引数:リトライする回数
第四引数:リトライする時の間隔の時間(ms)
上のコードを見てもらえれば想像つくかもしれませんが、第一引数のキーが変更になると再フェッチが行われます。
動かせば分かりますが、一度フェッチされた同じクエリーキーと紐づいた URL は、再フェッチされません。
実装したこと
- queryKey の実装
保持するキーに応じてキャッシュを持ちます - retry の実装
queryFn の取得に失敗した時に 指定した回数だけ再度リトライのリクエストを投げます - my-tanstack と react の繋ぎこみ
本当は useEffect と useState を使いたかったのですが、方法が分からなかったので、useSyncExternalStore を使いました。 - hooks化
これは簡単でした。
実装いていないこと
stale Time の実装
invalidate の実装
network コネクションの確認の実装
useMutation
など たくさん
技術詳細
tanstack の内部構造 は、下に挙げる記事を見れば内容を理解できるかもしれませんが、自分はコードを読むまではそこまでは理解できなかったので、そんな人の参考になれば嬉しいと思います。
簡単に全体を説明すれば、cache で キャッシュ(query)をもち、observer で cache を取得したり・設定周りも行ったり(原本では client で行っている)・画面の繋ぎ込みなどを行っています。
ここでは中身のロジックではなくて、cache・query・observer・state など tanstack query の元となるエンティティーの型を説明します。
cache
export class Cache {
queries: Map<string, Query<any, any>>;
...
}
cache が持っているのは、string がキー(内部で保持するキーで queryKey とは別物)で Query(後述)がバリューの Map 構造の queries のみです。
cache の中身は単純な Map なのでした。
query
export class Query<TData, TError> {
queryKey: string;
queryHash: string;
state: QueryState<TData, TError>;
retryCount: number;
retryDelay: number;
retryer?: Retryer<TData>;
promise?: Promise<TData>;
...
}
query で実際のフェッチした結果を保存します。
queryKey は ユーザーが指定したキー(上のサンプルコードで言うところの hoge
)、queryHash は 上の cache のキー部分を示します。
state の型 QueryState は以下のようになっていて、
export interface QueryState<TData, TError> {
data: TData | undefined;
dataUpdateCount: number;
error: TError | null | undefined;
errorUpdateCount: number;
fetchFailureCount: number;
status: QueryStatus;
}
ここの data にフェッチした結果が、status に success や pending などが入ります。ここの結果を、後の Observer で取得してフロントの React の世界に持ち込みます。
retry系(retryCount, retryDelay, retryer)は、リトライをするためのもので、promise に fetch をした時の結果が与えられます。
observer
export class Observer<TData, TError> extends Subscribable<any> {
currentQuery?: Query<TData, TError>;
currentQueryCache?: Cache;
currentQueryResult?: QueryObserverResult<TData, TError>;
queryKey: string;
queryFn: QueryFunction<TData, string>;
retryCount: number;
retryDelay: number;
state: QueryState<TData, TError>;
...
}
observer は、フロントエンドとの繋ぎ目で、フロントエンドで渡された設定を保持したり(本来は Client で実施)、現在の query や cache を保持したり、します。
余談ですが、observer が難しいのは、React の世界との連携なのですが、それは createResult の
this.listeners.forEach((listener) => {
listener(this.currentQueryResult)
})
で行っています(listener に useSyncExternalStore の onStoreChange が入ります)。
ここまでで簡単に型を見ましたが、複雑怪奇に見える tanstack の型もこれだけだと思えればなんとなく見れそうな気もしませんでしょうか?
my-tanstack-query も全体で 400行程度なので、ロジックも見れるなら見れば参考になる部分もあると思います。
ここまで読んでくださりありがとうございました!
ではでは、皆さんもいい開発ライフを!