Open14

TanStack Start を試してみる

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

プロジェクト作成

https://tanstack.com/router/latest/docs/framework/react/start/getting-started

コマンド
mkdir tanstack-start
cd tanstack-start
npm init -y
touch tsconfig.json
touch app.config.ts
npm i @tanstack/start @tanstack/react-router vinxi
npm i react react-dom @vitejs/plugin-react
npm i -D typescript @types/react @types/react-dom
tsconfig.json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "moduleResolution": "Bundler",
    "module": "ESNext",
    "target": "ES2022",
    "skipLibCheck": true,
  },
}
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({})
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

The Router Configuration

https://tanstack.com/router/latest/docs/framework/react/start/getting-started#the-router-configuration

この辺から写経をしていこう。

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>;
  }
}

何をしているかよくわからないがルーターの設定を行うファイルのようだ。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

The Server Entry Point

https://tanstack.com/router/latest/docs/framework/react/start/getting-started#the-server-entry-point

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);

ルーターの情報をサーバーに渡しているようだ。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

The Client Entry Point

https://tanstack.com/router/latest/docs/framework/react/start/getting-started#the-client-entry-point

app/client.tsx
import { hydrateRoot } from "react-dom/client";
import { createRouter } from "./router";
import { StartClient } from "@tanstack/start";

const router = createRouter();

hydrateRoot(document.getElementById("root")!, <StartClient router={router} />);

これによってクライアントサイドルーティングができるようになるようだ。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

The Root of Your Application

https://tanstack.com/router/latest/docs/framework/react/start/getting-started#the-root-of-your-application

app/routes/__root.tsx
import {
  createRootRoute,
  Outlet,
  ScrollRestoration,
} from "@tanstack/react-router";
import { Body, Head, Html, Meta, Scripts } from "@tanstack/start";

export const Route = createRootRoute({
  meta: () => [
    {
      charSet: "utf-8",
    },
    {
      name: "viewport",
      content: "width=device-width, initial-scale=1",
    },
    {
      title: "TanStack Start Starter",
    },
  ],
  component: RootComponent,
});

function RootComponent() {
  return (
    <RootDocument>
      <Outlet></Outlet>
    </RootDocument>
  );
}

function RootDocument({ children }: { children: React.ReactNode }) {
  return (
    <Html>
      <Head>
        <Meta></Meta>
      </Head>
      <Body>
        {children}
        <ScrollRestoration />
        <Scripts />
      </Body>
    </Html>
  );
}

RootComponent が全てのページのルートとなるようだ。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Writing Your First Route

https://tanstack.com/router/latest/docs/framework/react/start/getting-started#writing-your-first-route

コマンド
touch app/routes/index.tsx
app/routes/index.tsx
import { createFileRoute, useRouter } from "@tanstack/react-router";
import { createServerFn } from "@tanstack/start";
import { readFile, writeFile } from "fs/promises";

const filePath = "count.txt";

async function readCount() {
  return parseInt(await readFile(filePath, "utf-8").catch(() => "0"));
}

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

const updateCount = createServerFn("POST", async (addBy: number) => {
  const count = await readCount();
  await 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>
  );
}
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

エラー修正

app/routes/index.tsx(一部)
import { createFileRoute, useRouter } from "@tanstack/react-router";
import { createServerFn } from "@tanstack/start";
import * as fs from "fs";

const filePath = "count.txt";

async function readCount() {
  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}`);
});


これだと動くのはなぜだろう?

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

よくわからないが

import * as xxxx from 'yyyy' のように書けば良いのかな?

cyprto の randomInt 関数を使って同じようなことをやってみたが as を使うと成功する。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

おわりに

Next.js の代わりに Remix を使おうと思っていたけど TanStack Start も良さそう。

まだまだアルファ版なので仕事では使わない方が良いかも知れないが趣味の開発などで積極的に使っていこう。