這陣子工作專案使用 Next.js 開發前端頁面時,剛好遇到一個很常處理的需求:當使用者切換篩選條件、分頁或搜尋關鍵字時,希望能實現 Next.js URL 更新不刷新網頁,也就是隻同步變更網址列上的 GET 查詢參數(Query Parameters),但不重新執行伺服器端資料擷取或造成整個頁面閃爍重載。這裡隨手紀錄一下在 Pages Router 與 App Router 架構下的實作寫法與常見踩坑點。
💡 基礎前置觀念:若想先複習原生瀏覽器如何讀取網址各部位結構,可先參考站內整理好的 JavaScript 取得目前 URL 與 window.location 9 大屬性速查表。
一、Pages Router 架構:使用 next/router 的 shallow: true
在 Next.js 的 Pages Router(pages/ 目錄架構)中,若直接使用預設的 router.push() 修改網址,Next.js 會重新觸發頁面層級的 getServerSideProps 或 getStaticProps。
如果我們只想依據使用者操作對 URL 中的 GET 參數進行新增、修改或刪除,同時保持當前 React 元件的 State 不被重置,可以在 router.push 或 router.replace 的第三個選項參數傳入 { shallow: true }(淺層路由):
import React, { useEffect } from 'react';
import { useRouter } from 'next/router';
const FilterExample = () => {
const router = useRouter();
const updateQueryParams = () => {
// 確保 router 已完成 Hydration 再讀取 query
if (!router.isReady) return;
// 淺拷貝一份現有的 GET 參數物件,避免直接修改 router.query 原地屬性
const nextQuery = { ...router.query };
// 1. 新增或修改 GET 參數
nextQuery.type = 'frontend';
nextQuery.page = '2';
// 2. 刪除不需要的 GET 參數
delete nextQuery.key;
// 第二個參數 as 傳入 undefined,第三個參數設定 shallow: true
router.push(
{
pathname: router.pathname,
query: nextQuery,
},
undefined,
{ shallow: true }
);
};
return (
<button type="button" onClick={updateQueryParams}>
更新篩選參數(不刷新頁面)
</button>
);
};
export default FilterExample;
快速複製範本(Pages Router 單行版)
如果只需要快速在事件處理器中更新特定參數,可以直接複製以下結構:
router.push(
{
pathname: router.pathname,
query: { ...router.query, page: 2, sort: 'desc' },
},
undefined,
{ shallow: true }
);
⚠️ 注意
shallow: true的觸發限制:淺層路由僅在「同一個頁面路徑(相同pathname)」下變更參數時有效。若pathname切換到另一個路由頁面,Next.js 仍會卸載舊頁面並載入新頁面的資料。
二、App Router 架構:使用 next/navigation 與 URLSearchParams
在 Next.js 13.4+ 之後主流的 App Router(app/ 目錄架構)中,next/router 已被替換為 next/navigation,且原本的 { shallow: true } 選項已移除。在 Client Component 中,我們可以結合 usePathname、useSearchParams 與 router.replace(..., { scroll: false }),或是直接呼叫原生 window.history.pushState 來更新網址而不觸發畫面跳動:
'use client';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
export default function SearchFilter() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const handleFilterChange = (category) => {
// 將目前的 ReadonlyURLSearchParams 轉為可編輯的 URLSearchParams 實例
const params = new URLSearchParams(searchParams.toString());
if (category) {
params.set('category', category);
} else {
params.delete('category');
}
// 使用 replace 或 push 更新網址,並設定 scroll: false 避免畫面捲回頂部
router.replace(`${pathname}?${params.toString()}`, { scroll: false });
};
return (
<button type="button" onClick={() => handleFilterChange('react')}>
篩選 React 分類
</button>
);
}
若完全不需要觸發 Next.js Server Component 重新渲染,僅希望純客戶端同步網址列(例如連動本地 State 的頁籤切換),Next.js 官方亦支援在 App Router 直接使用瀏覽器原生的 window.history.pushState 或 window.history.replaceState:
const updateUrlClientOnly = (tabName) => {
const params = new URLSearchParams(window.location.search);
params.set('tab', tabName);
window.history.replaceState(null, '', `?${params.toString()}`);
};
三、Next.js 更新網址不刷新頁面的方案比較
以下整理不同路由架構與更新需求下的適用API對照:
| 方法與 API | 適用架構 | 是否保留捲動位置 | 常見適用情境 |
|---|---|---|---|
router.push(..., { shallow: true }) | Pages Router (next/router) | 預設捲回頂部(可加 scroll: false) | 同頁面篩選器、分頁切換且需保留返回歷史紀錄 |
router.replace(..., { scroll: false }) | App Router (next/navigation) | 是(透過 scroll: false 控制) | 搜尋框即時篩選、網址參數與 Server Component 同步 |
window.history.replaceState | Pages / App Router 皆可 | 是(純更新網址列) | 純前端頁籤(Tabs)切換、Modal 狀態同步至 URL |
四、實務開發常見的 2 個踩坑細節
- 直接修改
router.query物件屬性:在舊版程式碼中常看到直接寫const query = router.query; query.a = '1';。由於router.query是 Next.js 內部管理的物件參照,直接原地修改(Mutate)有時會導致比對失效或狀態汙染,務必先透過{ ...router.query }建立淺拷貝再修改。 useEffect初次渲染時router.query為空物件:在 Pages Router 採用靜態最佳化(Automatic Static Optimization)的頁面中,首次渲染時router.isReady為false,此時讀取router.query會拿到{}。若在useEffect內讀取並立刻執行router.push,會把網址原本帶有的參數洗掉,因此務必先判斷if (!router.isReady) return;。
發表迴響