紀錄工作經驗、相關知識,解決技術相關問題。

Javascript, Next, React, 新手 Jamp Holes, 網站相關

[前端筆記] Next.js URL 更新不刷新網頁:shallow routing 與 App Router 參數同步實作

[前端筆記] Next.js URL 更新不刷新網頁:shallow routing 與 App Router 參數同步實作 特色圖片

這陣子工作專案使用 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.replaceStatePages / App Router 皆可是(純更新網址列)純前端頁籤(Tabs)切換、Modal 狀態同步至 URL

四、實務開發常見的 2 個踩坑細節

  1. 直接修改 router.query 物件屬性:在舊版程式碼中常看到直接寫 const query = router.query; query.a = '1';。由於 router.query 是 Next.js 內部管理的物件參照,直接原地修改(Mutate)有時會導致比對失效或狀態汙染,務必先透過 { ...router.query } 建立淺拷貝再修改。
  2. useEffect 初次渲染時 router.query 為空物件:在 Pages Router 採用靜態最佳化(Automatic Static Optimization)的頁面中,首次渲染時 router.isReady 為 false,此時讀取 router.query 會拿到 {}。若在 useEffect 內讀取並立刻執行 router.push,會把網址原本帶有的參數洗掉,因此務必先判斷 if (!router.isReady) return;。

參考資料與延伸閱讀

發表迴響