😎

TanStack Start(alpha)をはじめよう

2024/08/31に公開

フルスタック React フレームワークである TanStack Start の使用方法について解説します。詳細はTanStack Start OverviewやGitHubリポジトリを参照してください。

TanStack Startとは

TanStack Startは、TanStack Routerを基盤としたフルスタックReactフレームワークです。サーバーサイドレンダリング(SSR)、ストリーミング、サーバー関数、バンドルなどの機能を提供します。

https://tanstack.com/start/latest

プロジェクトのステータス

TanStack Startは現在開発中ですが、すでに利用可能です。創設者のTanner Linsleyはalphaバージョンと呼んでいます。

ドキュメントはTanStack Router以下に公開されており、リポジトリにはサンプルコードも含まれています。

また、tanstack.comのサイトは実際にTanStack Startを使用してVercelにデプロイされています。

TanStack Start のセットアップ方法

1. 依存関係のインストール

TanStack StartはVinxiとTanStack Routerをベースにしており、これらを依存関係として必要とします。これらのインストールには、次のコマンドを実行します。

npm i @tanstack/start @tanstack/react-router vinxi

vinxiという見慣れないパッケージがありますが、これはTanStack Startが依存している重要なツールです。

Vinxi は「メタルーター」または「ルーターマネージャー」であり、サーバーとクライアントのルーターを一元的な設定でまとめることができます。Vite(開発サーバーとバンドル)と Nitro(HTTPサーバー)の上に構築されており、Solid Start のコミッターである Nikhil Saraf によって作成されました。

ReactとVite Reactプラグインも必要になるため、合わせてインストールします。

npm i react react-dom @vitejs/plugin-react

2. package.jsonの更新

package.json を更新して、新しいVinxiのエントリポイントを参照し、 "type": "module" を設定します。

{
 // ...
 "type": "module",
 "scripts": {
 "dev": "vinxi dev",
 "build": "vinxi build",
 "start": "vinxi start"
 }
}

3. TypeScriptの設定

TypeScriptを使用するために、以下のコマンドを実行してTypeScriptと型定義をインストールします。

npm i -D typescript @types/react @types/react-dom 

tsconfig.jsonは公式リポジトリを参考にして以下のように定義しました

{
  "include": ["**/*.ts", "**/*.tsx"],
  "compilerOptions": {
    "strict": true,
    "esModuleInterop": true,
    "jsx": "react-jsx",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "lib": ["DOM", "DOM.Iterable", "ES2022"],
    "isolatedModules": true,
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "target": "ES2022",
    "allowJs": true,
    "forceConsistentCasingInFileNames": true,
    "baseUrl": ".",
    "paths": {
      "~/*": ["./app/*"]
    },
    "noEmit": true
  }
}

4. app.config.tsの設定

次に、VinxiにTanStack Startの最小限の動作を開始するように指示するには、 app.config.ts ファイルを設定する必要があります。

// app.config.ts
import { defineConfig } from '@tanstack/start/config'

export default defineConfig({})

5. 必要なファイルの作成

TanStack Startを使用するには、次の4つのファイルが必要です。

  • ルーター設定(app/router.tsx): このファイルは、Start内で使用されるTanStack Routerの動作を決定します。ここでは、デフォルトのプリロード機能からキャッシュの有効期限まで、すべてを設定できます。
  • サーバーエントリポイント(app/ssr.tsx): TanStack StartはSSRフレームワークであるため、このルーター情報をサーバーエントリポイントに渡す必要があります。
  • クライアントエントリポイント(app/client.tsx): ルートがクライアントに解決したら、クライアント側のJavaScriptをハイドレートする方法が必要です。これは、同じルーター情報をクライアントエントリポイントに渡すことで行います。
  • アプリケーションのroot(app/routes/__root.tsx): 最後に、アプリケーションのルート(root)を作成する必要があります。これは、他のすべてのルートのエントリポイントです。このファイル内のコードは、アプリケーション内の他のすべてのルート(routes)をラップします。

設定が完了すると、ファイルツリーは次のようになります。

.
├── app/
│   ├── routes/
│   │   └── `__root.tsx`
│   ├── `client.tsx`
│   ├── `router.tsx`
│   ├── `routeTree.gen.ts`
│   └── `ssr.tsx`
├── `.gitignore`
├── `app.config.ts`
├── `package.json`
└── `tsconfig.json`

ルーター設定

ルーター設定 は app/router.tsx に作成します。

// app/router.tsx
import { createRouter as createTanStackRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'

export function createRouter() {
  const router = createTanStackRouter({
    routeTree,
  })
  return router
}

declare module '@tanstack/react-router' {
  interface Register {
    router: ReturnType<typeof createRouter>
  }
}

routeTree.gen.ts は、この時点ではまだ存在しないファイルです。これは、TanStack Startを( npm run dev または npm run start 経由で)初めて実行したときに生成されます。

サーバーエントリポイント

サーバーエントリポイント は app/ssr.tsx に作成します。

// app/ssr.tsx
import {
  createStartHandler,
  defaultStreamHandler,
} from '@tanstack/start/server'
import { getRouterManifest } from '@tanstack/start/router-manifest'
import { createRouter } from './router'

export default createStartHandler({
  createRouter,
  getRouterManifest,
})(defaultStreamHandler)

クライアントエントリポイント

クライアントエントリポイント は app/client.tsx に作成します。

import { hydrateRoot } from 'react-dom/client'
import { StartClient } from '@tanstack/start'
import { createRouter } from './router'

const router = createRouter()
hydrateRoot(document.getElementById('root')!, <StartClient router={router} />)

これで、基本的なテンプレートのセットアップが完了したので、最初のルートを作成できます。これは、 app/routes ディレクトリに新しいファイルを作成することで行います。

6. 最初のルートの作成

最初のルートは app/routes/index.tsx に作成します。ありふれたカウンターのデモを作成してみましょう。

// app/routes/index.tsx
import * as fs from 'fs'
import { createFileRoute, useRouter } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/start'

const filePath = 'count.txt'

async function readCount(): Promise<number> {
  return parseInt(
    await fs.promises.readFile(filePath, 'utf-8').catch(() => '0'),
  )
}

const getCount = createServerFn('GET', () => {
  return readCount()
})

const updateCount = createServerFn('POST', async (addBy: number) => {
  const count = await readCount()
  await fs.promises.writeFile(filePath, `${count + addBy}`)
})

export const Route = createFileRoute('/')({
  component: Home,
  loader: async () => await getCount(),
})

function Home() {
  const router = useRouter()
  const state = Route.useLoaderData()

  return (
    <button
      onClick={() => {
        updateCount(1).then(() => {
          router.invalidate()
        })
      }}
    >
      Add 1 to {state}?
    </button>
  )
}

これで、TanStack Startプロジェクトのセットアップと最初のルートの作成が完了しました。
npm run dev を実行してサーバーを起動し、 http://localhost:3000 にアクセスしてルートが動作していることを確認できます。

ルートの書き方

TanStack Startでは、以下の2つのルート定義方法があり、主にファイルベースのルーティングが推奨されています。

ファイルベースのルーティング

  • ルート定義にファイルシステムを使用します。
  • ルートは app/routes ディレクトリ内のファイル構造と命名規則に基づいて自動的に生成されます。
  • 例えば、 app/routes/blog/index.tsx は /blog に、 app/routes/blog/$postId.tsx は /blog/:postId のような動的なルートに対応します。
  • $ 記号は動的なルートセグメントを表し、 postId は useParams フックでアクセスできるルートパラメータになります。
  • ルートファイル内では、 createFileRoute 関数を使ってルートを定義します。
  • createFileRoute 関数には、パス、ローダー関数、コンポーネントなどを指定します。

ファイルベースのルーティングでは、ルート定義にファイルシステムを使用します。

ルートは app/routes ディレクトリ内のファイル構造と命名規則に基づいて自動的に生成されます。例えば、app/routes/blog/index.tsx/blog に、app/routes/blog/$postId.tsx/blog/:postId のような動的なルートに対応します。

$ 記号は動的なルートセグメントを表し、postIduseParams フックでアクセスできるルートパラメータになります。

ルートファイル内では、createFileRoute 関数を使ってルートを定義し、この関数にはパス、ローダー関数、コンポーネントなどを指定します。

export const Route = createFileRoute('/')({
  component: Home,
  loader: async () => await getCount(),
})

コードベースのルーティング

コードベースのルーティングは、すべてのルート定義をコードで記述する方法です。
createRoute 関数でルートオブジェクトを作成し、それを routeTree 配列に追加することでルートを定義します。
ファイルベースのルーティングの方が、ルート構造が明確になり、コードの可読性も高いため、一般的にはファイルベースのルーティングが推奨されます。

サーバー関数の使用方法

TanStack Startでは、サーバー関数はサーバーサイドで実行される関数であり、クライアントサイドから呼び出すことができます。
データベースとのやり取りや、外部APIへのアクセスなど、サーバーサイドで実行する必要がある処理を、クライアントサイドから透過的に実行できます。

サーバー関数の定義

サーバー関数は、 createServerFn 関数を使って定義します。
createServerFn 関数の最初の引数には、HTTPメソッドを指定します。
2番目の引数には、サーバー関数の実行内容を記述した関数を指定します。

// getServerTime.ts
import { createServerFn } from '@tanstack/start'

export const getServerTime = createServerFn('GET', async () => {
  // Wait for 1 second
  await new Promise((resolve) => setTimeout(resolve, 1000))
  // Return the current time
  return new Date().toISOString()
})

サーバー関数の呼び出し

サーバー関数は、クライアントサイドから useServerFn フックを使って呼び出すことができます。
useServerFn フックは、サーバー関数を返す関数を引数にとり、そのサーバー関数を呼び出すための関数を返します。

import { useServerFn } from '@tanstack/start'
import { useQuery } from '@tanstack/react-query'
import { getServerTime } from './getServerTime'

export function Time() {
  const getTime = useServerFn(getServerTime)

  const timeQuery = useQuery({
    queryKey: 'time',
    queryFn: () => getTime(),
  })
}

サーバー関数の実行とバンドル

TanStack Startでは、開発モードと本番モードでサーバー関数の扱いが異なります。

  • 開発モード: サーバー関数は、クライアントサイドのJavaScriptにバンドルされ、クライアントサイドで実行されます。
  • 本番モード: サーバー関数は、クライアントサイドのJavaScriptから抽出され、サーバーサイドでのみ実行されます。

クライアントサイドのサーバー関数は、サーバーに関数をリクエストを送信するプロキシ関数に置き換えられます。
サーバーサイドでは、サーバー関数はそのまま実行されます。
抽出後、各バンドルは未使用のコードを削除するデッドコード削除プロセスを適用します。

API ルートの定義方法

TanStack Start では、API ルートはサーバーサイドのエンドポイントを作成するための機能です。これはフォーム送信、ユーザー認証などの処理に使用できます。
プロジェクトの ./app/routes/api ディレクトリ内に定義され、TanStack Start サーバーによって自動的に処理されます
デフォルトでは/api というプレフィックスが付きます。このベースパスは、設定で apiBase を変更することでカスタマイズできます。

ファイルパス URL
routes/api.users.ts /api/users
routes/api/users.ts /api/users
routes/api/users.index.ts /api/users
routes/api/users/$id.ts /api/users/:id
routes/api/users/$id/posts.ts /api/users/:id/posts
routes/api.users.$id.posts.ts /api/users/:id/posts

API ルートハンドラの定義

API ルート は、createAPIFileRoute 関数を呼び出すことで、APIRoute インスタンスをエクスポートします。 TanStack Router の他のファイルベースのルートと同様に、この関数の最初の引数はルートのパスです。
各 HTTP メソッドハンドラは、次のプロパティを持つオブジェクトを受け取ります。

  • request: 標準的なRequest オブジェクト
  • params: ルートの動的パスパラメータを含むオブジェクト。 たとえば、ルートパスが /users/$id で、/users/123 に対してリクエストが行われた場合、params は { id: '123' } になります。

リクエストを処理したら、Response オブジェクトまたは Promise<Response> を返す必要があります。

// routes/api/hello.ts
import { createAPIFileRoute } from '@tanstack/start/api'

export const Route = createAPIFileRoute('/hello')({
  GET: async ({ request }) => {
    return new Response('Hello, World! from ' + request.url)
  },
})

動的パスパラメータ

API ルート は、$ の後にパラメータ名を続けることで表される動的パスパラメータをサポートしています。 たとえば、routes/api/users/$id.ts という名前のファイルは、動的な id パラメータを受け入れる /api/users/:id に API ルートを作成します。
1 つのルートに複数の動的パスパラメータを含めることもできます。 たとえば、routes/api/users/id/posts/postId.ts という名前のファイルは、2 つの動的パラメータを受け入れる /api/users/:id/posts/:postId に API ルートを作成します。

ワイルドカードパラメータ

API ルート は、パスの末尾にワイルドカードパラメータもサポートしており、$ の後に何も付けずに表されます。 たとえば、routes/api/file/$.ts という名前のファイルは、ワイルドカードパラメータを受け入れる /api/file/* に API ルートを作成します。

リクエストボディの処理

POST リクエストを処理するには、POST ハンドラをルートオブジェクトに追加します。 ハンドラは、最初の引数としてリクエストオブジェクトを受け取り、request.json() メソッドを使用してリクエストボディにアクセスできます。

export const Route = createAPIFileRoute('/hello')({
  GET: async ({ request }) => {
    return new Response(JSON.stringify({ message: 'Hello, World!' }), {
      headers: { 'Content-Type': 'application/json', },
    })
  },
})

request.text() や request.formData() などの他のメソッドを使用して、リクエストのボディにアクセスすることもできます。

SSR の仕組み

TanStack Start は、SSR を標準でサポートしています。 Nitro上に構築されているのでストリーミング SSR に対応します。

SSR の有効化

SSR を有効にするには、プロジェクトに app/ssr.tsx ファイルを作成します。
このファイルは、サーバーサイドレンダリングハンドラを作成する関数をエクスポートします。

// app/ssr.tsx
import {
  createStartHandler,
  defaultStreamHandler,
} from '@tanstack/start/server'
import { getRouterManifest } from '@tanstack/start/router-manifest'

import { createRouter } from './router'

export default createStartHandler({
  createRouter,
  getRouterManifest,
})(defaultStreamHandler)

ハイドレーションについて

SSRで内部的に取得したデータはcreateStartHandler によって自動的にハイドレートされます。
これをクライアントサイドで<StartClient>コンポーネントがデハイドレートします。
同じルーターの定義をサーバーとクライアントで共有することでTanStack Routerの既存の機能を使ってこれが実現されているのが興味深い点です。

デプロイについて

TanStack Start は、Vinxi と Vite を採用しているため、JS が実行できる場所であればどこにでもデプロイできます。

とはいえまずはVercelが勧められているようです。確かにvinxiビルドするとデフォルトでvercel buildも実行されました。

認証プロバイダについてもClerkが推薦されていました。このように各パーツごとに意見を持っているようです。

他にはデータベースはConvex、オブザーバビリティはSentryなどが挙げられていました。

筆者的にはDIYできるのがTanStackシリーズの利点なので好きに選ぶのがいいとおもいます。


Discussion