簡介

大家一直跟我說 TanStack Start 這東西很厲害,我想知道它是不是真的名副其實。最近我在各種研討會待了很多時間,TanStack Start 也常常被提起。社群對於 Server Components 到底是不是正解,意見很分歧。TanStack Start 則走了相反的路線,採用乾淨的、以客戶端優先的元件方式,而且可以用很多方法呼叫伺服器端程式碼,甚至同一個元件裡就能做到。

我平常大多是 Vue 和 Nuxt 使用者,所以我不是來踩任何框架的。我想弄清楚的問題更單純:TanStack Start 有哪些 Next.js 和 Nuxt 做不到的事,而且這些優點是否值得我改用它?

研究了一番之後,我整理出三個我非常喜歡 TanStack Start 的地方。光是這些特點,大概還不足以讓我立刻換框架,但我已經快被說服了。

如果你想改看影片,可以看這個!

https://www.youtube.com/watch?v=I59bnwcxmAo

前置需求

  • Node.js 22+
  • 熟悉 React 與 TypeScript
  • 不需要有 Next.js 或 TanStack 經驗

我們要做什麼

做一個 GitHub 使用者查詢工具。你輸入使用者名稱,應用程式就會在伺服器端透過 GitHub API 抓取該使用者資料,並渲染他的個人檔案。這非常適合拿來展示這三個功能。

完整程式碼可以在 demo repo 找到。先從建立應用程式開始。

步驟 1:建立應用程式

TanStack 有 CLI,所以建立骨架只要一個指令:

npx @tanstack/cli@latest create my-app --framework React

它會詢問套件管理器和幾個附加選項,接著用檔案式路由在 Vite 上建立專案。執行它:

cd my-app
npm install
npm run dev

我電腦上的開發伺服器不到一秒就準備好了。那種由 Vite 驅動的啟動速度真的很棒,也跟我先前 Vite 影片中提到的速度一樣。

專案結構很小:

src/
├── routes/
│   ├── __root.tsx      # 文件外殼
│   └── index.tsx       # 首頁路由
├── router.tsx          # 路由器設定
└── routeTree.gen.ts    # 自動產生,勿編輯

接著來看這三個特點。

功能 1:一個伺服器函式同時處理讀取與寫入

先公平地說,Next.js 其實也能直接呼叫伺服器程式碼。Next 有 React Server Functions,而在變更情境下,它們就是 Server Actions。你只要用 "use server" 標記一個函式,然後從元件中呼叫它,不需要 API 路由。Nuxt 也有自己的做法,透過 server/api 路由加上 useFetch,一樣可以拿到有型別的回應。所以「在伺服器上呼叫函式」這件事並不新鮮,也不是 TanStack Start 獨有。

不過,差別在於限制條件。Next.js 的 Server Actions 是以 POST 請求執行,主要是為了變更操作。Next 官方文件本身也會引導你在讀取資料時使用 Server Components 或 Route Handlers。你「可以」從客戶端元件呼叫 Server Action 來讀取資料,但它仍然是走 POST,不是慣用、可快取的 GET 路徑。

TanStack Start 不會這樣拆開。它只用一個基本原語 createServerFn,就能同時處理 GET 讀取和 POST 變更,而且你在任何地方都用同樣的方式呼叫它。

import { createServerFn } from '@tanstack/react-start'

interface GithubUser {
  login: string
  name: string | null
  avatar_url: string
  html_url: string
  bio: string | null
  public_repos: number
  followers: number
  following: number
}

const getGithubUser = createServerFn({ method: 'GET' })
  .inputValidator((username: string) => username)
  .handler(async ({ data: username }): Promise<GithubUser> => {
    const res = await fetch(`https://api.github.com/users/${username}`, {
      headers: {
        Accept: 'application/vnd.github+json',
        // 這裡的 token 會留在伺服器端,絕不會送到客戶端:
        // Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
      },
    })
    if (!res.ok) throw new Error(`找不到使用者 "${username}"`)
    return (await res.json()) as GithubUser
  })

這個 .handler 只會在伺服器端執行,所以 token 不會跑到瀏覽器裡。我把 method 設成 'GET',因為這是讀取操作,然後像一般非同步函式一樣直接在路由 loader 裡呼叫它。你不需要考慮 route handler、RSC 邊界,也不用維護 endpoint 字串的一致性。(這段程式碼為了清楚起見有做過裁切。repo 裡的版本會對輸入加上 encodeURIComponent,並且分開處理 404 與 403 rate limit 回應。)

你也可以在瀏覽器裡看到這件事。打開 network 分頁,執行一次查詢,你不會看到任何對 api.github.com 的請求。唯一的呼叫是送到我自己伺服器的那個。GitHub 的 fetch 是在伺服器端發生的,而這正是我想要的位置,而且任何 token 都不會外洩。

我很喜歡這裡的型別運作方式。我只需要在 handler 回傳值上註記一次 GithubUser,這個型別就會一路流到 loader 和元件裡,不必在每個呼叫處重複標註,無論是 GET 還是 POST 都一樣。只要把介面中的欄位名稱改掉,所有還在讀舊欄位的地方都會立刻標紅。(不過有一個注意點,這是編譯期的型別傳播,不是執行期驗證。我這裡是用 as GithubUser 轉型,所以如果你想確保外部 JSON 真的符合格式,還是應該加上執行期的 schema 檢查。)你也可以用 route handler 搭配共用型別或 schema 驗證做到這件事。差別在於,TanStack 預設就會幫你把整條鏈推導好,而不是要你自己手動串接。

功能 2:真正有型別的搜尋參數

這一點是目前 TanStack 真正有優勢的地方。Next.js 提供的是通用的字串與字串陣列搜尋參數,不是路由層級的 schema 驗證。useSearchParams 給你的是唯讀的 URLSearchParams,雖然 typedRoutes 加上 PageProps 輔助工具已經改善了路徑、導覽與頁面 props 的型別,但這些都沒有像 TanStack Router 那樣去驗證或轉換查詢字串裡面的「值」。你可以在 Next 裡搭配 Zod、nuqsnext-typesafe-url 達成,但那是你另外加上去的。Nuxt 可以透過 definePageMetavalidate 函式驗證路由,它可以回傳 falsePartial<NuxtError> 來拒絕路由,但它不會把 route.query 變成一個有型別、已驗證的查詢物件供元件使用。

TanStack Router 預設就把搜尋參數當成已驗證的路由狀態:

export const Route = createFileRoute('/')({
  validateSearch: (search): { user: string } => ({
    user: typeof search.user === 'string' ? search.user : '',
  }),
  loaderDeps: ({ search: { user } }) => ({ user }),
  loader: async ({ deps: { user } }) => {
    if (!user) return { user: null, error: null }
    return { user: await getGithubUser({ data: user }), error: null }
  },
  component: Home,
})

我只驗證一次 ?user=。之後 Route.useSearch() 在元件中的任何地方都會給我 { user: string },而且完全有型別。loader 讀取那個參數並執行伺服器函式,所以只要在 URL 裡載入 ?user=ErikCH,就會直接顯示個人檔案,不需要額外的客戶端接線。這個查詢可分享,也能在重新整理後保留,而且我完全沒有寫客戶端 state 就達成了。你也可以插入 Zod 來做更豐富的 schema 驗證。

功能 3:預設就一路到底的型別安全

只有 typed navigation 本身並不算獨特,這點我要說清楚。Next.js 有 typedRoutes 可以做靜態型別化連結,Nuxt 也透過 experimental.typedPages 內建 typed navigation,另外還有 nuxt-typed-router 模組可以補強。所以三者都能避免你把路由名稱打錯。

差別在於這條型別鏈能延伸多遠,以及你需要花多少設定。Next 的 typedRoutes 只會型別化路徑,不會管搜尋參數值。Nuxt 的 typed pages 是可選功能,涵蓋路由與參數。TanStack 則是預設就有,而且同一套型別系統會把你的路由參數、搜尋參數和 loader 資料串成一條連動的鏈。

const navigate = useNavigate({ from: Route.fullPath })

function lookup(e: React.FormEvent) {
  e.preventDefault()
  navigate({ search: { user: input.trim() } })  // 已型別化:{ user: string }
}

如果我傳入不存在的搜尋參數,或是型別不對,型別檢查器就會直接報錯。Vite 本身只會轉譯 TypeScript,不會做型別檢查,所以這部分是交給 tsc --noEmit(我把它放在 typecheck 腳本裡,並在 CI 執行)或由編輯器即時提示。又因為 loader 的回傳型別會流進 Route.useLoaderData(),我渲染的資料也會被同一條鏈型別化。從伺服器函式回傳值、經過 loader、搜尋參數,到連結的整條路徑,全部是一體的,而不是三個要你各自接線的功能。

加上 AI

我很常倚賴像 Kiro 這類 AI 程式助理來幫我寫程式碼,而 TanStack Start 又夠新,所以模型對它的了解不夠完整。當我請它產生 server function 時,有時候它會回傳較舊的 API 形狀,因為訓練資料已經落後了。

TanStack 針對這個問題直接提供了解法,也是我在影片裡最後展示的內容。有一個叫做 TanStack Intent 的套件,可以把你的 coding agent 接到最新的 TanStack 實作模式。安裝方式如下:

npx @tanstack/intent@latest install

這會建立或更新你的 agent 設定,預設是 AGENTS.md(也可以指定像 CLAUDE.md.cursorrules 之類的檔案),並加入技能載入指示。你的 agent 讀到之後,就會知道有哪些 TanStack 技能可用,並且針對它正在做的事情載入最新文件,而不是靠過時的訓練資料猜測。

所以我打開了 Kiro CLI,它會自己讀取那份 AGENTS.md,然後給了它這段指示:

請根據新載入的 TanStack Intent 規則,檢查我現有的儲存庫。請檢查我的實作是否有反模式、缺少邊界案例,或使用了已棄用的語法。

它跑完那些技能後回傳了一份清單,包括幾個 verbatimModuleSyntax 的註記、TanStack Start 的一些開發工具設定、還有一個 shell 元件相關的事項。最後還有一點是,我沒有在 package.json 裡釘住最新版本標籤。雖然不一定每次都必須這麼做,但我確實很喜歡它有在整體檢查 package.json

第二道防線就是前面提到的型別安全。當 AI 猜出舊版的 createServerFn 寫法時,tsc 和我的編輯器馬上就把它標出來了。我不用等到 code review 才發現,型別直接幫我抓到了。

這跟我在《Vue in the Age of AI》影片裡說的是同一個觀點。AI 現在寫了更多我們的程式,所以那些能替你驗證 AI 成果的框架,價值比以前更高。

清理

本機開發不用做任何清除。用 Ctrl+C 停止開發伺服器即可。如果你有部署,TanStack Start 底層是用 Nitro,所以你可以移除當初設定的任何 Node 目標。那些託管資源可能會產生費用,所以如果只是測試,記得把它們拆掉。

結論

所以它算不算很厲害?我的看法是這樣。如果你想要明確的控制、快速的 Vite 建置,以及從伺服器函式一路延伸到搜尋參數和連結的型別安全,TanStack Start 確實是我這段時間試過最有吸引力的 React 框架。光是伺服器函式和有型別的搜尋參數,就已經非常值得了。

但它還不是適合所有人。生態系比 Next.js 小,可用外掛較少,而且還很新。TanStack CLI 目前仍標示為 alpha,可參考的正式上線案例較少,部署與除錯知識也沒有 Next.js 那麼標準化。如果你需要 Next.js 那樣的招募人才池和部署故事,這確實是個值得等待的理由。

不過,「預設的 React 框架終於不再是唯一解」這件事,確實是多年來第一次成真。而在實際建置之後,我也能理解大家為什麼會轉過去。如果你像我一樣是 Nuxt 使用者,那麼有型別的搜尋參數和伺服器函式,會很像是那些你一直想要、卻不必再另外找模組補上的功能。

資源:


原文出處:https://dev.to/erikch/tanstack-start-is-kind-of-a-big-deal-4nec


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝7   💬1  
353
🥈
我愛JS
💬1  
3
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登