Table of Contents
App RouterでのTanStack Query
Next.js App RouterでTanStack Queryを使用する方法としてServer Componentでprefetchしてその後Hydrationを行う方法が公式ドキュメントでも紹介されています。
公式ドキュメントでのコードは以下のようになります。
// app/posts/page.jsx
import {
dehydrate,
HydrationBoundary,
QueryClient,
} from '@tanstack/react-query'
import Posts from './posts'
export default async function PostsPage() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: getPosts,
})
return (
// Neat! Serialization is now as easy as passing props.
// HydrationBoundary is a Client Component, so hydration will happen there.
<HydrationBoundary state={dehydrate(queryClient)}>
<Posts />
</HydrationBoundary>
)
}
ここでは以下の内容が行われています。
- Server Component上で
prefetchQuery()
を使用してprefetchする - HydrationBoundary(Client Component)でHydrationを行う
そしてPostsコンポーネントは以下のように書かれています。
// app/posts/posts.jsx
'use client'
export default function Posts() {
// This useQuery could just as well happen in some deeper
// child to <Posts>, data will be available immediately either way
const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts })
// This query was not prefetched on the server and will not start
// fetching until on the client, both patterns are fine to mix.
const { data: commentsData } = useQuery({
queryKey: ['posts-comments'],
queryFn: getComments,
})
// ...
}
PostsはhydrateしたデータをuseQuery()
経由で取得するClient Componentとなります。
ここで重要なのはqueryKey
がprefetchQuery()
のものと同じでなければならないことです。
2回目に実行したuseQuery()
のqueryKey
は一致しないため、ブラウザ上でしかデータフェッチされません。
このように、prefetch時とprefetchデータ利用時とで2度Queryを組み立てなければならないのと、それぞれqueryKey
を一致させなければなりません。
シンプルなアプリケーションの場合の多くは1つのAPIに対して複数のパターンのqueryKey
を扱うことはありません。その場合、上記例のようなposts
などになるはずです。
OpenAPIスキーマからのTanStack Queryコード生成
さて本題のOpenAPIスキーマからTanStack Queryコードを直接生成してどうNext.js App Routerに組み込むのかを説明します。
OpenAPIからTanStack Queryコードを生成するライブラリはいくつか存在しますが、特に設定不要で始められるのと、筆者がメンテしているというのもありOpenAPI React Query Codegenを使用します。
例えば以下のpetstore.yamlというスキーマファイルがあるとします。
openapi: "3.0.0"
info:
version: 1.0.0
title: Swagger Petstore
description: A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification
termsOfService: http://swagger.io/terms/
contact:
name: Swagger API Team
email: apiteam@swagger.io
url: http://swagger.io
license:
name: Apache 2.0
url: https://www.apache.org/licenses/LICENSE-2.0.html
servers:
- url: http://petstore.swagger.io/api
paths:
/pets:
get:
description: |
Returns all pets from the system that the user has access to
Nam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem sagittis sit amet. Aenean at gravida augue, ac iaculis sem. Curabitur odio lorem, ornare eget elementum nec, cursus id lectus. Duis mi turpis, pulvinar ac eros ac, tincidunt varius justo. In hac habitasse platea dictumst. Integer at adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante molestie imperdiet. Vivamus id aliquam diam. Cras quis velit non tortor eleifend sagittis. Praesent at enim pharetra urna volutpat venenatis eget eget mauris. In eleifend fermentum facilisis. Praesent enim enim, gravida ac sodales sed, placerat id erat. Suspendisse lacus dolor, consectetur non augue vel, vehicula interdum libero. Morbi euismod sagittis libero sed lacinia.
Sed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condimentum ligula luctus nec. Phasellus semper velit eget aliquet faucibus. In a mattis elit. Phasellus vel urna viverra, condimentum lorem id, rhoncus nibh. Ut pellentesque posuere elementum. Sed a varius odio. Morbi rhoncus ligula libero, vel eleifend nunc tristique vitae. Fusce et sem dui. Aenean nec scelerisque tortor. Fusce malesuada accumsan magna vel tempus. Quisque mollis felis eu dolor tristique, sit amet auctor felis gravida. Sed libero lorem, molestie sed nisl in, accumsan tempor nisi. Fusce sollicitudin massa ut lacinia mattis. Sed vel eleifend lorem. Pellentesque vitae felis pretium, pulvinar elit eu, euismod sapien.
operationId: findPets
parameters:
- name: tags
in: query
description: tags to filter by
required: false
style: form
schema:
type: array
items:
type: string
- name: limit
in: query
description: maximum number of results to return
required: false
schema:
type: integer
format: int32
responses:
'200':
description: pet response
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Pet'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
post:
description: Creates a new pet in the store. Duplicates are allowed
operationId: addPet
requestBody:
description: Pet to add to the store
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NewPet'
responses:
'200':
description: pet response
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/not-defined:
get:
deprecated: true
description: This path is not fully defined.
responses:
default:
description: unexpected error
post:
deprecated: true
description: This path is not defined at all.
responses:
default:
description: unexpected error
/pets/{id}:
get:
description: Returns a user based on a single ID, if the user does not have access to the pet
operationId: find pet by id
parameters:
- name: id
in: path
description: ID of pet to fetch
required: true
schema:
type: integer
format: int64
responses:
'200':
description: pet response
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
delete:
description: deletes a single pet based on the ID supplied
operationId: deletePet
parameters:
- name: id
in: path
description: ID of pet to delete
required: true
schema:
type: integer
format: int64
responses:
'204':
description: pet deleted
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
Pet:
allOf:
- $ref: '#/components/schemas/NewPet'
- type: object
required:
- id
properties:
id:
type: integer
format: int64
NewPet:
type: object
required:
- name
properties:
name:
type: string
tag:
type: string
Error:
type: object
required:
- code
- message
properties:
code:
type: integer
format: int32
message:
type: string
その場合OpenAPI React Query Codegenを使う場合は以下コマンドを実行します。
npx @7nohe/openapi-react-query-codegen -i ./petstore.yaml
実行後にはopenapi
というディレクトリができ、その配下にrequests
とqueries
というディレクトリが作成されます。
詳しい説明は省きますがそれぞれ以下のようなファイル群となります。
requests
: ピュアなTSクライアントコード(ReactやTanStackに関わらない)queries
: TanStack Queryに関するコード
この生成されたコードを先ほどのTanStackの公式ドキュメントのようにApp Routerに組み込むと以下のようになります。
// app/pets/page.tsx
import { prefetchUseDefaultServiceFindPets } from "../../openapi/queries/prefetch";
import {
HydrationBoundary,
QueryClient,
dehydrate,
} from "@tanstack/react-query";
import Pets from "./pets";
export default async function PetsPage() {
const queryClient = new QueryClient();
await prefetchUseDefaultServiceFindPets(queryClient);
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<Pets />
</HydrationBoundary>
);
}
Petsコンポーネント側はどうなるかというと、
// app/pets/pets.tsx
"use client";
import { useDefaultServiceFindPets } from "../../openapi/queries";
export default function Pets() {
const { data } = useDefaultServiceFindPets();
// ...
}
となります。
実際はパラメータとしてlimitやtagsを渡すことになると思いますが、queryKey
を考えて定数を用意して、queryFn
へ渡すためのデータフェッチ関数をprefetchQuery()
とuseQuery()
へそれぞれimportしてセットする作業を省くことができました。
これによりqueryKey
がずれてうまくHydrationできなかったということが減ります。
実際に画面上で動かしてDevtoolsから確認してみてもQueryが一度しか呼ばれてないことが確認できますし、Networkタブで見てもServer Renderingされているのがわかると思います。
以上でNext.js App RouterでのTanStack Query利用方法の説明と、OpenAPI React Query Codegenによる実装省力化の手法を簡単に紹介しました。 このライブラリはまだまだ育てている段階ですので、アイデアなどあればお待ちしております!