loader (Route loaders)loader 參數 (loader Parameters)loader 消費資料 (Consuming data from loaders)loaderDeps 存取搜尋參數 (Using loaderDeps to access search params)staleTime 控制資料被視為新鮮的時間 (Using staleTime to control how long data is considered fresh)shouldReload 和 gcTime 選擇退出快取 (Using shouldReload and gcTime to opt-out of caching)資料載入是網頁應用程式的常見需求,且與路由密切相關。在載入應用程式頁面時,最理想的狀況是所有頁面的非同步需求都能盡早並行地獲取與滿足。路由是協調這些非同步依賴的最佳位置,因為它通常是應用程式中唯一能在內容渲染前就知道用戶將前往何處的地方。
您可能熟悉 Next.js 的 getServerSideProps 或 Remix/React-Router 的 loader。TanStack Router 具有類似的功能,可以並行地預載/載入每個路由的資源,讓它能在透過 suspense 獲取資料時盡快渲染。
除了這些路由器的基本功能外,TanStack Router 更進一步提供了內建的 SWR 快取 (SWR Caching),這是一個長期記憶體快取層,用於路由載入器。這意味著您可以使用 TanStack Router 預載路由資料使其瞬間載入,或暫時快存取訪過的路由資料以供後續使用。
每次檢測到 URL/歷史記錄更新時,路由器會執行以下順序:
TanStack 的路由快取很可能適合大多數中小型應用程式,但了解使用它與更強大的快取解決方案(如 TanStack Query)之間的權衡很重要:
TanStack Router 快取的優點:
TanStack Router 快取的缺點:
Tip
如果您已確定需要使用更強大的工具如 TanStack Query,請直接跳至外部資料載入指南。
路由器快取是內建的,只需從任何路由的 loader 函數返回資料即可使用。讓我們學習如何使用!
路由 loader 函數在路由匹配載入時被呼叫。它們接收一個參數,這是一個包含許多有用屬性的物件。我們稍後會詳細介紹這些屬性,但首先讓我們看一個路由 loader 函數的範例:
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
})
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
})
loader 函數接收一個包含以下屬性的物件:
使用這些參數,我們可以做很多酷炫的事情,但首先讓我們看看如何控制它以及何時呼叫 loader 函數。
要從 loader 消費資料,請使用 Route 物件上定義的 useLoaderData 鉤子。
const posts = Route.useLoaderData()
const posts = Route.useLoaderData()
如果無法直接存取您的路由物件(例如您位於當前路由的元件樹深處),可以使用 getRouteApi 存取相同的鉤子(以及 Route 物件上的其他鉤子)。這應該比直接導入 Route 物件更受青睞,因為後者可能會造成循環依賴。
import { getRouteApi } from '@tanstack/react-router'
// 在您的元件中
const routeApi = getRouteApi('/posts')
const data = routeApi.useLoaderData()
import { getRouteApi } from '@tanstack/react-router'
// 在您的元件中
const routeApi = getRouteApi('/posts')
const data = routeApi.useLoaderData()
TanStack Router 為路由載入器提供了一個內建的過期重新驗證 (Stale-While-Revalidate) 快取層,其鍵值基於路由的依賴:
使用這些依賴作為鍵值,TanStack Router 會快取從路由的 loader 函數返回的資料,並用它來滿足對相同路由匹配的後續請求。這意味著如果路由的資料已在快取中,它會立即返回,然後可能在背景重新獲取,具體取決於資料的「新鮮度」。
為了控制路由依賴和「新鮮度」,TanStack Router 提供了大量選項來控制路由載入器的鍵值和快取行為。讓我們按您最可能使用的順序來看看它們:
想像一個 /posts 路由透過搜尋參數 offset 和 limit 支援分頁。為了讓快取能唯一儲存這些資料,我們需要透過 loaderDeps 函數存取這些搜尋參數。透過明確標識它們,每個具有不同 offset 和 limit 的 /posts 路由匹配不會混淆!
一旦我們有了這些依賴,路由將在依賴變化時總是重新載入。
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
loaderDeps: ({ search: { offset, limit } }) => ({ offset, limit }),
loader: ({ deps: { offset, limit } }) =>
fetchPosts({
offset,
limit,
}),
})
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
loaderDeps: ({ search: { offset, limit } }) => ({ offset, limit }),
loader: ({ deps: { offset, limit } }) =>
fetchPosts({
offset,
limit,
}),
})
預設情況下,導航的 staleTime 設為 0 毫秒(預載為 30 秒),這意味著路由的資料總是會被視為過期,並且在路由匹配和導航到時總會在背景重新載入。
**這對大多數使用情境來說是個好預設,但您可能會發現某些路由資料更靜態或可能載入成本較高。**在這些情況下,您可以使用 staleTime 選項來控制路由資料對導航保持新鮮的時間。讓我們看一個範例:
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
// 將路由資料視為新鮮 10 秒
staleTime: 10_000,
})
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
// 將路由資料視為新鮮 10 秒
staleTime: 10_000,
})
透過將 10_000 傳遞給 staleTime 選項,我們告訴路由器將路由資料視為新鮮 10 秒。這意味著如果使用者在最後一次載入結果的 10 秒內從 /about 導航到 /posts,路由的資料不會重新載入。如果使用者在 10 秒後從 /about 導航到 /posts,路由的資料將在背景重新載入。
要為路由停用過期重新驗證快取,請將 staleTime 選項設為 Infinity:
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
staleTime: Infinity,
})
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
staleTime: Infinity,
})
您甚至可以透過在路由器上設定 defaultStaleTime 選項來為所有路由關閉此功能:
const router = createRouter({
routeTree,
defaultStaleTime: Infinity,
})
const router = createRouter({
routeTree,
defaultStaleTime: Infinity,
})
類似於 Remix 的預設功能,您可能希望配置路由僅在進入或關鍵載入器依賴變化時載入。您可以透過結合使用 gcTime 選項和 shouldReload 選項來實現這一點,後者接受一個布林值或一個函數,該函數接收與 beforeLoad 和 loaderContext 相同的參數,並返回一個布林值,指示路由是否應重新載入。
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
loaderDeps: ({ search: { offset, limit } }) => ({ offset, limit }),
loader: ({ deps }) => fetchPosts(deps),
// 在卸載後不快取此路由的資料
gcTime: 0,
// 僅在使用者導航到或依賴變化時重新載入路由
shouldReload: false,
})
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
loaderDeps: ({ search: { offset, limit } }) => ({ offset, limit }),
loader: ({ deps }) => fetchPosts(deps),
// 在卸載後不快取此路由的資料
gcTime: 0,
// 僅在使用者導航到或依賴變化時重新載入路由
shouldReload: false,
})
即使您選擇退出路由資料的短期快取,您仍然可以獲得預載的好處!使用上述配置,預載仍會「開箱即用」並使用預設的 preloadGcTime。這意味著如果一個路由被預載,然後導航到,路由的資料將被視為新鮮且不會重新載入。
要選擇退出預載,請不要透過 routerOptions.defaultPreload 或 routeOptions.preload 選項開啟它。
我們在外部資料載入頁面分解了這個使用案例,但如果您想使用像 TanStack Query 這樣的外部快取,可以透過將所有載入器事件傳遞給您的外部快取來實現。只要您使用預設值,唯一需要做的更改是將路由器上的 defaultPreloadStaleTime 選項設為 0:
const router = createRouter({
routeTree,
defaultPreloadStaleTime: 0,
})
const router = createRouter({
routeTree,
defaultPreloadStaleTime: 0,
})
這將確保每個預載、載入和重新載入事件都會觸發您的 loader 函數,然後可以由您的外部快取處理和去重複。
傳遞給 loader 函數的 context 參數是一個物件,包含以下內容的合併:
從路由器的頂部開始,您可以透過 context 選項將初始上下文傳遞給路由器。此上下文將對路由器中的所有路由可用,並在每個路由匹配時被複製和擴展。這是透過 beforeLoad 選項將上下文傳遞給路由來實現的。此上下文將對該路由的所有子路由可用。最終的上下文將對路由的 loader 函數可用。
在這個範例中,我們將在路由上下文中建立一個函數來獲取文章,然後在我們的 loader 函數中使用它。
🧠 上下文是依賴注入的強大工具。您可以使用它來注入服務、鉤子和其他物件到您的路由器和路由中。您還可以使用路由的 beforeLoad 選項在每個路由上遞增地傳遞資料。
export const fetchPosts = async () => {
const res = await fetch(`/api/posts?page=${pageIndex}`)
if (!res.ok) throw new Error('Failed to fetch posts')
return res.json()
}
export const fetchPosts = async () => {
const res = await fetch(`/api/posts?page=${pageIndex}`)
if (!res.ok) throw new Error('Failed to fetch posts')
return res.json()
}
import { createRootRouteWithContext } from '@tanstack/react-router'
// 使用 createRootRouteWithContext<{...}>() 函數建立根路由,並傳遞您希望在路由器上下文中可用的任何類型。
export const Route = createRootRouteWithContext<{
fetchPosts: typeof fetchPosts
}>()() // 注意:雙重調用是有意的,因為 createRootRouteWithContext 是一個工廠 ;)
import { createRootRouteWithContext } from '@tanstack/react-router'
// 使用 createRootRouteWithContext<{...}>() 函數建立根路由,並傳遞您希望在路由器上下文中可用的任何類型。
export const Route = createRootRouteWithContext<{
fetchPosts: typeof fetchPosts
}>()() // 注意:雙重調用是有意的,因為 createRootRouteWithContext 是一個工廠 ;)
import { createFileRoute } from '@tanstack/react-router'
// 注意我們的 postsRoute 如何引用上下文來獲取 fetchPosts 函數
// 這可以是一個強大的工具,用於在您的路由器和路由之間進行依賴注入。
export const Route = createFileRoute('/posts')({
loader: ({ context: { fetchPosts } }) => fetchPosts(),
})
import { createFileRoute } from '@tanstack/react-router'
// 注意我們的 postsRoute 如何引用上下文來獲取 fetchPosts 函數
// 這可以是一個強大的工具,用於在您的路由器和路由之間進行依賴注入。
export const Route = createFileRoute('/posts')({
loader: ({ context: { fetchPosts } }) => fetchPosts(),
})
import { routeTree } from './routeTree.gen'
// 使用您的 routerContext 建立一個新的路由器
// 這將要求您滿足 routerContext 的類型要求
const router = createRouter({
routeTree,
context: {
// 將 fetchPosts 函數提供給路由器上下文
fetchPosts,
},
})
import { routeTree } from './routeTree.gen'
// 使用您的 routerContext 建立一個新的路由器
// 這將要求您滿足 routerContext 的類型要求
const router = createRouter({
routeTree,
context: {
// 將 fetchPosts 函數提供給路由器上下文
fetchPosts,
},
})
要在 loader 函數中使用路徑參數,請透過函數參數的 params 屬性存取它們。以下是一個範例:
// routes/posts.$postId.tsx
export const Route = createFileRoute('/posts/$postId')({
loader: ({ params: { postId } }) => fetchPostById(postId),
})
// routes/posts.$postId.tsx
export const Route = createFileRoute('/posts/$postId')({
loader: ({ params: { postId } }) => fetchPostById(postId),
})
將全局上下文傳遞給您的路由器很好,但如果您想提供特定於路由的上下文呢?這就是 beforeLoad 選項的用武之地。beforeLoad 選項是一個函數,在嘗試載入路由之前運行,並接收與 loader 相同的參數。除了其重定向潛在匹配、阻止載入器請求等能力外,它還可以返回一個物件,該物件將被合併到路由的上下文中。讓我們看一個範例,我們透過 beforeLoad 選項將一些資料注入到路由上下文中:
// /routes/posts.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts')({
// 將 fetchPosts 函數傳遞給路由上下文
// /routes/posts.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts')({
// 將 fetchPosts 函數傳遞給路由上下文
Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.