把舊版動態頁搬到新版Next.js後,原本的 params.slug不能照常使用,型別也提示params像一個Promise。先確認專案版本與這個頁面使用App Router。現行動態路由的params是非同步值,Server Component要先等待它,再從解析出的物件取slug。
這不需要把整頁改成Client Component,也不代表網址裡的slug需要先呼叫某個API。問題在框架提供參數的介面變了。本文用兩條不同網址驗證同一個動態頁,再分清路徑參數、查詢參數與靜態生成,避免看到Promise就改錯資料來源。
動態資料夾的名字決定key
在App Router中,app/items/[slug]/page.js的中括號資料夾代表一個動態區段。請求 /items/alpha 時,slug的值是alpha;請求 /items/bravo 時則是bravo。若資料夾叫 [id],解析物件的key就叫id,不會因為你的變數寫slug就自動更名。
export default async function ItemPage({ params }) {
const { slug } = await params
return <main><h1>{slug}</h1></main>
}
Server頁可以是async函式。在函式裡解開params,再把普通字串用於查詢或渲染。這份最小頁只顯示字串,不查資料庫,也不把字串當成可以任意存取的檔案路徑;先核到這層,再接回真正的內容查詢。
容易寫錯的是 await params.slug。它在await之前就先嘗試讀params上的slug;你需要等的是params這個Promise本身。拆成兩行 const resolved = await params與 resolved.slug也可以,只要先後順序清楚。
TypeScript也要標出Promise
如果原頁面的型別仍寫成普通物件,會把舊假設留在編輯器提示裡。新版頁面型別可以把非同步參數直接表達出來。
export default async function ItemPage({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
return <main><h1>{slug}</h1></main>
}
這個型別對應單一 [slug]區段。catch-all區段是陣列等其他形狀,不能把每一種動態路由都標成string。依資料夾規則選型別,再驗證業務上允許的值;型別描述不等於使用者輸入已符合你的產品規則。
官方文件指出,Next.js14與更早版本的params是同步prop,15保留過同步存取以協助相容,但這種方式會被淘汰。本文真案例使用16.3.8,採現行非同步寫法。維護舊站時先看實際依賴與升級指南,不要對所有版本一律套同一個錯誤訊息。
用兩個slug測,避免只看固定字串
最小測試專案只需基本layout與上述動態頁。啟動後分別請求alpha與bravo,確認兩個回應都正常,而且各自顯示正確值。只測alpha一個網址,可能漏掉程式把測試值硬寫在畫面的錯誤。
curl http://127.0.0.1:3000/items/alpha
curl http://127.0.0.1:3000/items/bravo
本文在Next.js16.3.8、React19.3.0的隔離App Router案例中,兩個請求都回200,h1分別是alpha與bravo。這證明params解析與頁面路由對上;它沒有證明任何商品或會員資料已存在,因為範例沒有接業務資料。
接回網站時,先用已知存在的資料ID測查詢成功,再用不存在的ID測無資料分支。網址參數正確與查詢結果有效,是接續的兩件事。若slug解析正確而查不到文章,應查資料與查詢條件,不要再把問題歸給await。
如果兩個網址都顯示同樣內容,也要看是否查詢時寫死slug、外層元件保留了不應共用的狀態,或內容沒有依參數選取。本文的兩值比較就是要先排除最基本的路由資料使用問題,而不是只核HTML有字。
params與searchParams來自不同位置
/items/alpha?page=2的alpha來自動態路徑,page來自查詢字串。在Server page裡,框架另外提供searchParams;現行介面同樣需要依文件處理Promise,但它不會出現在params裡。
export default async function ItemPage({ params, searchParams }) {
const { slug } = await params
const query = await searchParams
return <main>{slug} / page={query.page ?? '1'}</main>
}
這只是把兩個來源分開讀的示意,沒有處理page是否為有效頁碼。真實查詢參數可能重複、缺席或不符合預期型別,仍要依頁面要求驗證。不能把查詢參數直接拿來組任意SQL或伺服器檔案路徑。
如果只需要slug,不必因為看到範例而多讀searchParams。保持資料需求最小,也讓維護者容易看出本頁為什麼要等待哪些值。網址參數並不是所有頁面都必須一起使用的套餐。
Route Handler也有自己的context.params
在動態API路由,參數位於處理函式的context,而不是Server page的props。現行Route Handler文件同樣把context.params列為會解析成參數物件的Promise。
export async function GET(request, { params }) {
const { slug } = await params
return Response.json({ slug })
}
這段示意的是另一條合適的動態API路由,不應與同一區段的page共用相同路徑。若已經有 /items/[slug]頁面,可以把API放在 /api/items/[slug],保持頁面與資料回應職責清楚。資料取得是否需要權限,也要在自己的處理流程中核對。
Client Component不能直接改成async渲染來照抄Server頁。官方動態路由文件說明可用React的 use解開傳來的Promise,或者依元件位置使用適合的路由hook。若頁面原本在伺服器查資料,沒有其他理由就不必只為了params搬到客戶端。
generateStaticParams不會取代await
generateStaticParams的用途是提供要靜態產生的參數集合,頁面函式怎麼取得目前params則是另一件事。即使你為alpha、bravo列出靜態參數,現行頁面仍應依介面先await params再讀slug,不能把兩個API的責任混在一起。
export function generateStaticParams() {
return [{ slug: 'alpha' }, { slug: 'bravo' }]
}
本文的兩個請求案例沒有加這個函式,避免把靜態產生與參數解析混為同一個測試。是否要預先生成、未列出的路徑怎麼處理、資料多久更新,要按你的網站內容與部署方式決定,不能只因為slug數量少就直接假設。
修舊頁時,先核版本、資料夾key、await位置與兩個不同網址的輸出。這些都對上,再接回查詢、無資料回應與快取策略。讓每一步回答一個明確問題,通常比把整頁搬去Client或到處加Promise包裝更容易找到真正缺口。
多段路徑不要直接當成單一slug
[slug]與 [...slug]不是一樣的路由。後者收集後面的多個區段,解析出的slug會是陣列;例如某個文件路徑有分類與項目兩段,就不能用單一字串的處理方式把它當成一筆文章名稱。若是可選catch-all,還需要照顧沒有那些區段時的情況。
這些形狀改變不影響「先await params」的順序,卻會改變之後怎麼使用資料。處理函式應先依路由定義分清字串或陣列,再決定要找哪筆內容,不要用強制型別斷言把所有輸入壓成string,讓編輯器沒報錯就當成已正確。
如要把多段區段組成自己的內容識別,請依產品規則驗證每段。路徑來自請求,並不因為框架幫你解析就自動成為可存取的檔案名稱或已授權的資源。範例用固定合成值顯示文字,真站的資料查找仍要遵守自己的來源與權限條件。
最後,型別檢查與HTTP測試都值得保存。型別能發現Promise與物件的介面不符,兩個真請求能核對路徑值是否真的進到畫面;兩者回答不同問題。先讓這個最小頁通過,再恢復完整查詢,會更容易知道新增的故障出在哪一段。

評論0