EY-Office ブログ

再度TanStack Startを評価してみた、フルスタック・フレームワークとしての完成度はかなり高そう

昨年、ブログAlpha版のTanStack Startを使ってみました!に書いた時点ではTanStack StartはAlpha版でしたが、現在は RC(リリース候補) になっています。そこで最新のTanStack Startを評価してみました。

TanStackStartRC Nano Banana Proが生成した画像を使っています

Alpha版とRC版の違い

まずQuick Startにある方法でプロジェクトを作成し、昨年作ったコードをコピーして動かしましたが、当然のように動きませんでした。ドキュメントやサンプルコードを見ながらコードを修正してみましたが、結果としては少しの修正で動きました。

主な修正点、

src/routes/index.tsx

Server Functionの引数がdata:パラメーターのオブジェクトに変わっていました

await ponScore(human + 1);  // Bug?await ponScore({data: { human }});
src/utils/JyankeFunction.ts

Server Functionの定義も変わっていました。処理関数をcreateServerFnの戻り値のhandler関数で定義するようになりました。

export const getScores = createServerFn('GET', async() => {
  return readScores();
})export const getScores = createServerFn({method:'GET'})
  .handler(async() => {
    return readScores();
})

引数のバリデーション機能inputValidatorが追加されました。厳密なバリデーションにはZodなども利用できます。

export const ponScore = createServerFn('POST', async (human1: Te) => {
  const human = human1 - 1;                            // Bug?
  const computer = Math.floor(Math.random() * 3);
  const judgment = (computer - human + 3) % 3;
  const score = {human, computer, judgment} as ScoreType;
  writeScores(score);
})export const ponScore = createServerFn({method:'POST'})
  .inputValidator((data: { human: Te }) => data)
  .handler(async ({data}) => {
    const human = data.human;
    const computer = Math.floor(Math.random() * 3);
    const judgment = (computer - human + 3) % 3;
    const score = {human, computer, judgment} as ScoreType;
    await writeScores(score);
})
ディレクトリー構成

いくつかの設定ファイルに変更がありましたが、アプリケーションの部分は app/ディレクトリーが src/ になったくらいでしょうか。

├── .cta.json
├── package-lock.json
├── package.json
├── public
│   └── ...
├── src
│   ├── components
│   │   ├── JyankenBox.tsx
│   │   └── ScoreBox.tsx
│   ├── router.tsx
│   ├── routes
│   │   ├── __root.tsx
│   │   └── index.tsx
│   ├── routeTree.gen.ts
│   ├── styles.css
│   └── utils
│       ├── JyankeFunction.ts
│       ├── JyankeType.ts
│       └── prisma.ts
├── tsconfig.json
└── vite.config.ts

.cta.jsonにはnpm create @tanstack/start@latestコマンドで作ったプロジェクトの設定が書かれています。

PrismaやTailwind CSSを使うように変更してみた

良く使われているPrismaやTailwind CSSが使えるか確認してみました。実はnpm create @tanstack/start@latestでプロジェクトを作成する際の選択肢でどちらも指定できます。

$ npm create @tanstack/start@latest

> npx
> "create-start"

┌  Let's configure your TanStack Start application
│
◇  What would you like to name your project?
│  test1
│
◇  Would you like to use Tailwind CSS?
│  Yes
│
◇  Select toolchain
│  None
│
◇  Select deployment adapter
│  Nitro (agnostic)
│
◇  What add-ons would you like for your project?
│  Prisma
│
◇  Would you like any examples?
│  none
│
◇  Prisma: Database Provider
│  SQLite
│
◇  Initialized git repository
│
◇  Installed dependencies
│
└  Your TanStack Start app is ready in 'test1'.
src/routes/index.tsx

GET /で起動される、ジャンケンのメイン・コンポーネントの定義です。

  • ① TanStack Startはデフォルトでサーバーサイドレンダリング(SSR)なので、通常ssr: trueは不要ですが説明のために追加しました
  • loader:の値はジャンケン結果の取得コードです
    • ssr: trueの場合はloader:はサーバーで実行され、サーバーサイド・レンダリングされた結果のHTMLがブラウザーに送られます(取得したデータもハイドレーションのためにブラウザーに送られます)
    • ssr: falseの場合はブラウザーでReactが実行され、サーバーファンクションとして実行され、ブラウザーに戻されたデータでクライアント・レンダリングされます
import { createFileRoute, useRouter } from '@tanstack/react-router'
import ScoreBox from '@/components/ScoreBox';
import JyankenBox from '@/components/JyankenBox';
import { getScores, ponScore } from '@/utils/JyankeFunction';
import { Te } from '@/utils/JyankeType';

export const Route = createFileRoute('/')({
  ssr: true,                                  // ← ①
  component: Home,
  loader: async () => await getScores(),      // ← ②
})

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

  const pon = async (human: Te) => {
    await ponScore({data: { human }});
    router.invalidate();
  }

  return (
    <div className="mx-8 w-1/2">
      <h1 className="my-6 text-center text-xl font-bold">じゃんけん ポン!</h1>
      <JyankenBox pon={pon} />
      <ScoreBox scores={scores} />
    </div>
  );
}
src/utils/JyankeFunction.ts

サーバーファンクションのコードをファイルからPrismaに変更しました。最初にnpm create @tanstack/start@latest プロジェクト名でプロジェクトを作成したのでPrismaは入らないのでPrismaを追加しました。
さらに、PrismaがV7にアップデートしていましたので少し戸惑いましたが、Quickstart with Prisma ORM and SQLiteを参考にインストールと設定を行いました。

  • ① このprisma.tsファイルは7. Instantiate Prisma Clientにあるコードです
  • ② 上でも説明しましたが、inputValidatorで引数(パラメーター)のバリデーションが行えます、またTypeScriptの型付けもここで行われます。また認証やロギング等のミドルウエアを組み込む事もできます。
import { createServerFn } from '@tanstack/react-start'
import { ScoreType, Te } from './JyankeType';

import { prisma } from './prisma';                  // ← ①

async function readScores(): Promise<ScoreType[]> {
  const scores = await prisma.scores.findMany({orderBy: {id: 'desc'}, take: 10});
  return scores as ScoreType[];
}

async function writeScores(score: ScoreType) {
  await prisma.scores.create({ data: score });
}

export const getScores = createServerFn({method:'GET'})
  .handler(async() => {
    return readScores();
  }
)

export const ponScore = createServerFn({method:'POST'})
  .inputValidator((data: { human: Te }) => data)     // ← ②
  .handler(async ({data}) => {
    const human = data.human;
    const computer = Math.floor(Math.random() * 3);
    const judgment = (computer - human + 3) % 3;
    const score = {human, computer, judgment} as ScoreType;
    await writeScores(score);
    return true;
  }
)

他のファイルはAlpha版のTanStack Startを使ってみました!にTailwind CSS対応を追加しただけです。

TanStack Startは完成度が高いフルスタック・フレームワーク

今回のサンプルコードはシンプルなので登場しませが、TanStack Startはフルスタック・フレームワークを実現するためのいろいろな機能が用意されています。

ドキュメントのExecution ModelにTanStack Startの実行モデルが解説されています。

  • 原則は、コードはクライアント(ブラウザー)でもサーバーでも動くべき — Isomorphic
  • ただし、どちらかでしか実行出来ない関数は
    • サーバーのみなら createServerFnで定義
    • クライアントのみなら createClientOnlyFnで定義
  • サーバーとクライアントで動作を変えたい場合はcreateIsomorphicFnで定義します
    • 例: サーバーならファイルを使い、クライアントならlocalStorageを使うstorage関数
const storage = createIsomorphicFn()
  .server((key: string) => {
    // Server: File-based cache
    const fs = require('node:fs')
    return JSON.parse(fs.readFileSync('.cache', 'utf-8'))[key]
  })
  .client((key: string) => {
    // Client: localStorage
    return JSON.parse(localStorage.getItem(key) || 'null')
  })

Next.jsのモジュールでは"use server""use client"でサーバーで動くのかクライアントで動くのか指定します。非常にシンプルでサーバー・ファンクションもローカルな関数も同じように定義でき扱えます。
"use ..."してないモジュールはサーバー・クライアント両方で動かせます。しかし環境変数のアクセスのような基本機能もサーバー(process.env.)とクライアント(import.meta.env.)では異なり困ります。

TanStack StartではこのようなケースでもcreateIsomorphicFnで解決できます。
Alpha版のころにはこのような機能はなかったと思われす、しかしTanStack Startが進化していく段階でこのような機能の必要性がわかり実装されたものと思います。

まとめ

Next.jsはサーバー・クライアントの違いをあまり意識せずに書けます、その裏ではコンパイラーやランタイムライブラリーが頑張って対応しています。ただしNext.jsではスマートに書けなくなった時やパフォーマンス等に問題が出た場合は、解決が見えないレイヤーを相手にするので大変になります。

TanStack Startは動作がよく見える構造なので書かなければいけないコードは少し増えますが、パフォーマンス等の問題は対応がしやすいと思います。また1年かけてAlpha版からRCまで進化し続け、すでに完成度の高いフルスタック・フレームワークなのかもしれないと思えるようになりました。

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ