🧊

`useQuery({ initialData })`が指定されると、`query.data`の型は`TData | undefined`ではなく`TData`

説明が難しいシリーズ。

const query = useQuery({
  queryKey: ['todo', id],
  queryFn: () => fetchTodo(id),
  initialData: { id: 0, title: 'xxx' }, // <- コレ 
});

デバッグ用くらいにしか使ったことなくて気にしてなかったけど、場合によってはハマるなーと思ったので書いておく。

いつものパターン

まずはおさらい。

const query = useQuery({
  queryKey: ['todo', id],
  queryFn: () => fetchTodo(id),
});

この場合に、query.data.xxxを取得するためには、query.isSuccessであることを確認した上でアクセスするのが定石。

// この時点ではまだローディング中で、`undefined`の可能性がある
query.data?.title;

// 成功してたら安全
if (query.isSuccess) {
  query.data.title;
}

これぞTanstack Queryって感じ。

initialDataがあると

しかし。

const query = useQuery({
  queryKey: ['todo', id],
  queryFn: () => fetchTodo(id),
  initialData: { id: 0, title: 'xxx' }, // <- コレ 
});

こうなってると、なんと最初からundefinedは排除されてる。

// この時点でも、キャッシュがあるので`undefined`の可能性はない
query.data.title;

// いつも通りにも書ける
if (query.isSuccess) {
  query.data.title;
}

ふむ。

better type narrowing for initialData · Issue #3310 · TanStack/query https://github.com/TanStack/query/issues/3310

何がハマるかというと

コードベース全体で見たときに、この指定があるクエリとないクエリという2種類が生まれることになる。

isSuccessdataによって型ガードをやってるものと、そうでないものが生まれるってこと。

<Dummy p={aQuery.data.xxx} />

{bQuery.isSuccess && (
  <Dummy p={bQuery.data.xxx} />
)}

個人的には型が変わると思ってなかったので、なんでこのクエリだけ型ガードなしで動いてんの?!TSの秘孔でもついた?ってなって混乱して時間を浪費した。

ちなみに、queryFn単体だと、データを同期で返してもそうはならない。

const query = useQuery({
  queryKey: ['todo', id],
  queryFn: () => ({ id: 1, title: "Why?" })
});

query.data?.title;

placeholderDataの場合も、当然そうはならない。

const query = useQuery({
  queryKey: ['todo', id],
  queryFn: () => fetchTodo(id),
  placeholderData: { id: 0, title: 'xxx' },
});

query.data?.title;

事前にキャッシュを食わせたいなら

ドキュメントにも書いてあるけど、これにはいくつかやり方がある。

Initial Query Data | TanStack Query Docs https://tanstack.com/query/latest/docs/framework/react/guides/initial-query-data

  • インラインでinitialDataを渡す
  • 事前にprefetchQuery()しておく
  • 事前にsetQueryData()を使う

後の2つでは、型は変わらない。

クエリのインラインでinitialDataを指定したときだけ、型が絞り込まれる。だからややこしい。

どういうケースで遭遇したか

あまり一般的なケースではないと思うけど、

という場面で、なんでこのクエリだけ型エラーにならんの?なんで??ってなってた。

そもそも暗黙的にインラインオプション指定したクエリを、それが見えないままグローバルに共有するなって話かもしれない。

画一的にしたいのなら、やはり非同期側に揃っててほしい(多少の型ガード記述は冗長になるけど)な〜って思った日だった。 後からAPI化したときも直す必要ないし。