BetaになったTanStack Startを触るぞ!!!
はじめに
こんにちは!Reactのフルスタックフレームワークといえば、Next.jsが広く知られていますが、新たにOSSのTanStackが「TanStack Start」というフレームワークをリリースしました(現在はbeta版が公開されています!)
この記事では、TanStack Startのセットアップ手順や主要な機能について解説していきます。
TanStack Startとは
Vinxiを基盤に構築された、フルスタックなReactフレームワークで、主な機能にはサーバーサイドレンダリング(SSR)ストリーミング、サーバー機能などが含まれます。そして主にそれらはTanStack Routerが90%以上を占め、残りの5%はその他の機能で構成されています。(Youtube参照)
詳しくは以下からご確認ください。
セットアップ
それでは、ローカル環境にTanStack Startをインストールして、実際に動かしてみましょう!以下の手順に従ってセットアップを行います。
インストール
新規のフォルダを作成します。
mkdir myApp
cd myApp
bun init
tsconfig.jsonを作成してください。
{
"compilerOptions": {
"jsx": "react-jsx",
"moduleResolution": "Bundler",
"module": "ESNext",
"target": "ES2022",
"skipLibCheck": true,
"strictNullChecks": true,
},
}
TanStack Start はVinxiとTanStack Routerが必要なのでinstallします。
bun add @tanstack/start @tanstack/react-router vinxi
ReactとViteのReact pluginをインストールします。
bun add react react-dom
bun add -d @vitejs/plugin-react
必要に応じてTypescriptを追加でダウンロードしていきます。
bun add -d typescript @types/react @types/react-dom
package.jsonをアップデート + app.configを作成
Vinxiを入れたのでpackage.json
をアップデートします。
{
// ...
"type": "module",
"scripts": {
"dev": "vinxi dev",
"build": "vinxi build",
"start": "vinxi start"
}
}
app.config.ts
を作成してください。
import { defineConfig } from '@tanstack/start/config'
export default defineConfig({})
フォルダ構成
以下が作成した後の構成になります。
├── app/
│ ├── routes/
│ │ └── __root.tsx
│ ├── client.tsx
│ ├── router.tsx
│ ├── routeTree.gen.ts
│ └── ssr.tsx
├── .gitignore
├── app.config.ts
├── package.json
└── tsconfig.json
基本のテンプレートを追加
TanStack Startをスタートさせるには以下の4つのファイルが必要です。
- router.tsx
- ssr.tsx
- client.tsx
- __root.tsx
Router Configを作成
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>
}
}
Server Entry Pointを作成
/// <reference types="vinxi/types/server" />
import {
createStartHandler,
defaultStreamHandler,
} from '@tanstack/start/server'
import { getRouterManifest } from '@tanstack/start/router-manifest'
import { createRouter } from './router'
export default createStartHandler({
createRouter,
getRouterManifest,
})(defaultStreamHandler)
Client Entry Pointを作成
/// <reference types="vinxi/types/client" />
import { hydrateRoot } from 'react-dom/client'
import { StartClient } from '@tanstack/start'
import { createRouter } from './router'
const router = createRouter()
hydrateRoot(document, <StartClient router={router} />)
rootを作成
app配下にroutesフォルダを作成してその中に__root.tsx
を作成してください。
import {
Outlet,
ScrollRestoration,
createRootRoute,
} from '@tanstack/react-router'
import { Meta, Scripts } from '@tanstack/start'
import type { ReactNode } from 'react'
export const Route = createRootRoute({
head: () => ({
meta: [
{
charSet: 'utf-8',
},
{
name: 'viewport',
content: 'width=device-width, initial-scale=1',
},
{
title: 'TanStack Start Starter',
},
],
}),
component: RootComponent,
})
function RootComponent() {
return (
<RootDocument>
<Outlet />
</RootDocument>
)
}
function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
return (
<html>
<head>
<Meta />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
)
}
ローカルで起動させる
index.tsxをroutes配下に作成してみてください!(中は空っぽでも大丈夫です!)
├── app/
│ ├── routes/
│ │ ├── __root.tsx
│ │ └── index.tsx ← ここ
│ ├── client.tsx
│ ├── router.tsx
│ ├── routeTree.gen.ts
│ └── ssr.tsx
├── .gitignore
├── app.config.ts
├── package.json
└── tsconfig.json
では、準備ができたのでサーバーを起動させていきましょう。
bun run dev
そうです。index.tsxはファイル作成しただけで何も記載していないのに
Hello "/"
と表示されていますね!以下のルーティングについてでも説明していますが、サーバーを起動した際に自動でテンプレートをセットしてくれます。
TanStack Startの特徴
ここから数点TanStack Startの良いなと思った点を見ていきます。
ルーティングについて(File-based Route Generation)
TanStack StartではFile-based Route Generationになります。昨今のWebフレームワークではFile-basedでのルーティングが多いと思います。TanStack Startも同じです。
一度、posts.tsx
を作成していきましょう。
├── app/
│ ├── routes/
│ │ ├── __root.tsx
│ │ └── posts.tsx ← ここ
│ ├── client.tsx
│ ├── router.tsx
│ ├── routeTree.gen.ts
│ └── ssr.tsx
posts.tsxをapp/routes/posts.tsx
に作成します。
すると、以下のようにTanStack側が自動で生成してくれます。これがFile-based Route Generationとなります。
ルーティングについて
ルーティングについてはTanStack Routerの部分でもあるので
こちらで詳細を確認して頂ければと思います。
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/posts")({
component: () => <div>Hello "/posts"!</div>,
});
Server Functions
続いてはTanStack Startの中でも大きな特徴であるServer Functionsについてです。
Server Functionsとは何か
Server Functionsは、サーバー上でのみ実行されるどこからでも呼び出すことが可能な関数のことです。
Server Functionsの特徴としては
- HTTPでアクセスすることはできない
- ローダー、フック、コンポーネントなどどこからでも呼び出すことができます。
また、Next.jsではサーバー側で実行したい関数をuse server
と明記する必要がありますが、TanStack Startではその必要はありません。
Server Functionを作成する
Server Functionをapp配下に今回はserverFunctions.ts
を作成します。ファイル名は特に指定はありません。
今回はJSONPlaceholderから「記事」をfetchしてくるサーバー関数を作成します。
import { createServerFn } from '@tanstack/start';
import axios from "axios";
export const fetchPost = createServerFn({ method: 'GET' })
.validator((d: PostId) => d)
.handler(async ({ data }) => {
const post = await axios
.get(
`https://jsonplaceholder.typicode.com/posts/${data.postId}`
)
.then((r) => r.data)
.catch((err) => {
console.error(err);
if (err.status === 404) {
throw notFound();
}
throw err;
});
return post;
});
基本的にシンタックスとしてはシンプルだと思います。createServerFn()
を呼び出して返り値をhandler()
に記述していきます。
createServerFn()
の引数にはmethodを入れることが可能でGET
とPOST
を使うことができます。
バリデーション
Server Functionではクライアントから受け取った値に対してバリデーションをすることができます。
クライアントとサーバー間でのやり取りをする上で型を保証することはとても大切なことで、特にユーザーからの入力などを扱う場合はとても重要です。
バリデーションは自分で実装することも可能ですが今回はバリデーションライブラリのzod
を使用していきます。
zod install
bun add zod
import { notFound } from '@tanstack/react-router';
import { createServerFn } from '@tanstack/start';
import axios from 'axios';
type PostId = {
postId: string;
};
type Post = {
id: number;
title: string;
body: string;
};
export const fetchPost = createServerFn({ method: 'GET' })
.validator((d: PostId) => d)
.handler(async ({ data }) => {
const post = await axios
.get<Post>(`https://jsonplaceholder.typicode.com/posts/${data.postId}`)
.then((r) => r.data)
.catch((err) => {
console.error(err);
if (err.status === 404) {
throw notFound();
}
throw err;
});
return post;
});
Middleware
Server Functionに対してMiddlewareを使用することも可能です。MiddlewareはcreateMiddleware()
を呼び出して作成していきます。
const loggingMiddleware = createMiddleware().server(async ({ next, data }) => {
console.log('Request received:', data)
const result = await next()
console.log('Response processed:', result)
return result
})
またMiddlewareはクライアント側で実行することも可能で、client contextをserver側に送信することも可能です。
const requestLogger = createMiddleware()
.client(async ({ next, context }) => {
return next({
sendContext: {
// workspaceIdをserver側に送信
workspaceId: context.workspaceId,
},
})
})
.server(async ({ next, data, context }) => {
// contextでclient側からの受け取りが可能に
console.log('Workspace ID:', context.workspaceId)
return next()
})
実際にServer Functionを呼んでみる
では上記で作成したfetchPostを呼べるようなページを作成してきましょう。postIdをquery paramsで受け取り、ページ遷移時に取得します。
まずはpostsを受け取るページを作成します。
├── app/
│ ├── routes/
│ │ ├── __root.tsx
│ │ ├── index.tsx
│ │ ├── posts.tsx ← ここ
│ │ └── posts_.$postId.tsx
今回はpostsを取得するServer Functionは同一ファイルで作成して呼びます。この際、Server Functionはファイルのトップレベルで呼ぶ必要があるので一番上に定義します。
import { createFileRoute, Link } from '@tanstack/react-router';
import { createServerFn } from '@tanstack/start';
import axios from 'axios';
const fetchPosts = createServerFn({ method: 'GET' }).handler(async () => {
console.info('Fetching posts...');
return axios
.get('https://jsonplaceholder.typicode.com/posts')
.then((r) => r.data.slice(0, 10));
});
export const Route = createFileRoute('/posts')({
loader: async () => fetchPosts(),
component: RouteComponent,
});
function RouteComponent() {
const posts = Route.useLoaderData();
console.log(posts);
return (
<>
<div>Hello "/posts"!</div>
{posts.map((post) => (
<div key={post.id}>
<h2>
<Link
to="/posts/$postId"
params={{
postId: post.id,
}}
>
{post.title}
</Link>
</h2>
</div>
))}
</>
);
}
タイトルを表示できたところで各postsの詳細を取得するために、postIdをquery paramsで受け取り、postの中身を表示するページを作成していきます。
├── app/
│ ├── routes/
│ │ ├── __root.tsx
│ │ ├── index.tsx
│ │ ├── posts.tsx
│ │ └── posts_.$postId.tsx ← ここ
import { createFileRoute } from '@tanstack/react-router';
import { fetchPost } from '../serverFunctions';
export const Route = createFileRoute('/posts_/$postId')({
component: PostComponent,
loader: ({ params: { postId } }) => fetchPost({ data: { postId } }),
});
function PostComponent() {
const post = Route.useLoaderData();
return (
<div>
<h4>{post.title}</h4>
<div>{post.body}</div>
</div>
);
}
loaderで受け取った値はRoute.useLoaderDataで取得することが可能です。
クライアント側でServer Functionを呼ぶ方法
上記ではloaderでServer Functionを呼び出しましたがuseServerFn()
を呼び出し引数としてServer Functionを入れてあげることでクライアント側で呼び出すことが可能です。以下の様にuseQueryとの併用をすることもできます。
export function Time() {
const getTime = useServerFn(getServerTime)
const timeQuery = useQuery({
queryKey: 'time',
queryFn: () => getTime(),
})
}
API Routes
TanStack Startでも別のサーバを必要とせずにアプリケーション内にサーバサイドエンドポイントを作成できます。
TanStack Startではapiハンドラーを作成する必要があるのでapp配下に作成していきます。
import {
createStartAPIHandler,
defaultAPIFileRouteHandler,
} from '@tanstack/start/api'
export default createStartAPIHandler(defaultAPIFileRouteHandler)
apiはapp/routes配下に作成します。呼び出しはcreateAPIFileRoute()
を記述することです。以下のコードはサーバーが起動している場合はファイルを作成した時点でセットアップしてくれます。
import { createAPIFileRoute } from '@tanstack/start/api'
export const APIRoute = createAPIFileRoute('/hello')({
GET: async ({ request }) => {
return new Response('Hello, World! from ' + request.url)
},
})
Tanstack Startのこれから
現状はbetaですが、これからRSC(React Server Components)の対応も今後していくと11月のReact Summit USでも製作者のTannarさんがコードをスライドに載せていましたね!
const $renderPosts = createServerFn().handler(async () => {
const posts = await fetchPosts();
return (
<div>
{posts.map((post) => (
<Post post={post} />
))}
</div>
);
});
終わり
TanStack Start は、執筆時点(2024年1月)ではベータ版ですが、Next.js や Remix をはじめとする多様な React フレームワークの中で、TanStack Start がどのように変化していくのか注目したいところです!
TanStack 製ライブラリとの高い互換性は、開発者にとって魅力的な特徴の一つです。今後のアップデートと進化に期待したいです!!
参考
Discussion