準備
TypeScript + Vite環境で試してみます。
最初にライブラリのインストール。
$ npm install @tanstack/react-router
今回はAPIの取得も行うのでaxiosとreact-queryもインストールします。
$ npm install axios @tanstack/react-query
使用する詳細なバージョン。
@tanstack/react-query: 5.56.2
@tanstack/react-router: 1.57.15
axios: 1.7.7
typescript: 5.5.3
vite: 5.4.1
ファイルベースルーティング
ルーティングの方法としては従来からのルーテンングファイルを作成する方法とファイル構造から自動的にルーティングするファイルベースの方法があります。
今回はファイルベースのルーティングを試してみます。
各ページのコンポーネントファイルから作成しましょう。
ファイルを作成するディレクトリは設定で変更できますが、初期設定ではroutes
ディレクトリの中に作成します。
src/routes/index.tsx
import { createFileRoute } from '@tanstack/react-router' export const Route = createFileRoute('/')({ component: Index, }) function Index() { return ( <div> <h1>ホーム</h1> </div> ) }
src/routes/about.tsx
import { createFileRoute } from '@tanstack/react-router' export const Route = createFileRoute('/about')({ component: About, }) function About() { return ( <div> <h1>自己紹介</h1> </div> ) }
__root.tsx
というファイルは各ページで共通で表示されるファイルです。
createRootRouteWithContext
にはページのコンポーネントがなかった時のコンポーネント設定とかをしています。
表示するコンポーネントは各ページへリンクするナビゲーションとを配置。Outlet
には先ほど作成したページに該当するコンポーネントが表示されます。
src/routes/__root.tsx
import { Link, Outlet, createRootRouteWithContext } from '@tanstack/react-router' export const Route = createRootRouteWithContext()({ component: RootComponent, notFoundComponent: () => { return ( <div> <p>アクセスしたURLは存在しません。</p> <Link to="/">TOPへ戻る</Link> </div> ) }, }) function RootComponent() { return ( <> <nav> <Link to="/">Home</Link> <Link to="/about">About</Link> <Link to="/posts">Post</Link> </nav> <Outlet /> </> ) }
routeTree.gen.tsの生成
ファイルベースルーティングでも実はそのままでは動作しなくrouteTree.gen.ts
というファイルを作成する必要があります。
ファイルを生成する方法は保存時に自動的に出力する方法とコマンドラインから手動で出力する方法があります。
今回はViteを使用しているので、vite.config.ts
を編集して保存時に自動的に出力する設定にします。
vite.config.ts
import { defineConfig } from 'vite' import react from '@vitejs/plugin-react-swc' import { TanStackRouterVite } from '@tanstack/router-plugin/vite' export default defineConfig({ plugins: [ TanStackRouterVite(), react() ], resolve: { alias: { '@': '/src', }, }, })
TanStackRouterVite
というプラグインを追加するとファイル編集時に自動的にsrc/routeTree.gen.ts
ファイルが生成されます。
その他の出力方法に関しては下記ページを参考にしてください。
App.tsxの編集
最後にApp.tsx
を編集します。
src/App.tsx
import { RouterProvider, createRouter } from '@tanstack/react-router' import { routeTree } from './routeTree.gen' const router = createRouter({ routeTree }) declare module '@tanstack/react-router' { interface Register { router: typeof router } } function App() { return ( <RouterProvider router={router} /> ) } export default App
ブラウザでアクセスするとHomeとAboutボタンが表示され、クリックするとそれぞれのページへ遷移できるようになります。
APIからデータ取得
次はAPIからデータを取得して表示してみます。
App.tsx
でTanstackQueryを使えるようにする設定とTanstackRouterも少し変更を行います。
src/App.tsx
import { RouterProvider, createRouter } from '@tanstack/react-router' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { routeTree } from './routeTree.gen' const queryClient = new QueryClient() const router = createRouter({ routeTree, context: { queryClient, }, defaultPendingMs: 100, defaultPendingComponent: () => { return ( <div>読み込み中</div> ) }, defaultErrorComponent: () => { return ( <div>エラー</div> ) }, }) declare module '@tanstack/react-router' { interface Register { router: typeof router } } function App() { return ( <QueryClientProvider client={queryClient}> <RouterProvider router={router} /> </QueryClientProvider> ) } export default App
__root.tsx
も編集してQueryClient
をコンテキストで扱えるようにします。
src/routes/__root.tsx
import { Link, Outlet, createRootRouteWithContext } from '@tanstack/react-router' import type { QueryClient } from '@tanstack/react-query' export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({ component: RootComponent, notFoundComponent: () => { return ( <div> <p>アクセスしたURLは存在しません。</p> <Link to="/">TOPへ戻る</Link> </div> ) }, })
createRouter
のパラメータはdefaultPendingMs
がローディング表示を開始する速度で、defaultPendingComponent
がローディング中のコンポーネントを設定します。
デフォルトだとローディング表示のタイミングが少し遅かったので調節しています。
次はAPIの取得処理をまとめたファイルを作ります。
今回APIはjsonplaceholder
を使って一覧と詳細取得の関数を作成します。
src/api/posts.ts
import axios from 'axios'; import { notFound } from '@tanstack/react-router' import { queryOptions } from '@tanstack/react-query' export type Post = { id: string title: string body: string } export const fetchPost = async (postId: string) => { return await axios .get<Post>(`https://jsonplaceholder.typicode.com/posts/${postId}`) .then(response => response.data) .catch(error => { if (error.status === 404) { throw notFound() } throw error }) } export const fetchPosts = async () => { return await axios .get<Post[]>('https://jsonplaceholder.typicode.com/posts') .then(response => response.data.slice(0, 10)) } export const postsQueryOptions = queryOptions({ queryKey: ['posts'], queryFn: () => fetchPosts(), }) export const postQueryOptions = (postId: string) => queryOptions({ queryKey: ['posts', { postId }], queryFn: () => fetchPost(postId), })
一覧画面
この処理を使って一覧を表示してみましょう。
useSuspenseQuery
を使うとローディング処理とか入れなくていいのでシンプルに書けます。
src/routes/posts.tsx
import { createFileRoute, Link, Outlet } from '@tanstack/react-router' import { useSuspenseQuery } from '@tanstack/react-query' import { postsQueryOptions } from '@/api/posts' export const Route = createFileRoute('/posts')({ loader: ({ context: { queryClient } }) => queryClient.ensureQueryData(postsQueryOptions), component: PostsComponent, }) function PostsComponent() { const { data: posts } = useSuspenseQuery(postsQueryOptions) return ( <div> <h3>Posts</h3> <ul> {posts.map(post => ( <li key={post.id}> <Link to="/posts/$postId" params={{ postId: String(post.id) }}> {post.title.substring(0, 20)} </Link> </li> ))} </ul> <Outlet /> </div> ) }
詳細画面
同じように詳細画面も作成します。
src/routes/posts/$postId.tsx
import { createFileRoute, useRouter } from '@tanstack/react-router' import { useSuspenseQuery } from '@tanstack/react-query' import { postQueryOptions } from '@/api/posts' export const Route = createFileRoute('/posts/$postId')({ loader: ({ context: { queryClient }, params: { postId } }) => queryClient.ensureQueryData(postQueryOptions(postId)), component: PostComponent, }) function PostComponent() { const postId = Route.useParams().postId const { data: post } = useSuspenseQuery(postQueryOptions(postId)) return ( <div> <h4>{post.title}</h4> <div>{post.body}</div> </div> ) }
これで/posts
にアクセスすると一覧表示され、それぞれのページに遷移できるようになります。
簡単でしたが今回は以上になります。