準備

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にアクセスすると一覧表示され、それぞれのページに遷移できるようになります。

簡単でしたが今回は以上になります。