Open14
TanStack Start を試してみる
このスクラップについて
TanStack Router のドキュメントを読んでいたら無性に試したくなったので少し試してみよう。
プロジェクト作成
コマンド
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({})
Add the Basic Template
コマンド
mkdir -p app/routes
touch app/routes/__root.tsx
touch app/client.tsx
touch app/router.tsx
touch app/routeTree.gen.ts
touch app/ssr.tsx
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>;
}
}
何をしているかよくわからないがルーターの設定を行うファイルのようだ。
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);
ルーターの情報をサーバーに渡しているようだ。
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} />);
これによってクライアントサイドルーティングができるようになるようだ。
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 が全てのページのルートとなるようだ。
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>
);
}
サーバー起動
コマンド
npm run dev
エラー発生
Module "fs/promises" has been externalized for browser compatibility. Cannot access "fs/promises.readFile" in client code. See https://vitejs.dev/guide/troubleshooting.html#module-externalized-for-browser-compatibility for more details.
エラー修正
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}`);
});
これだと動くのはなぜだろう?
この辺が関係ありそうな気がする。
よくわからないが
import * as xxxx from 'yyyy'
のように書けば良いのかな?
cyprto の randomInt 関数を使って同じようなことをやってみたが as を使うと成功する。
おわりに
Next.js の代わりに Remix を使おうと思っていたけど TanStack Start も良さそう。
まだまだアルファ版なので仕事では使わない方が良いかも知れないが趣味の開発などで積極的に使っていこう。