在App Router的page加了一顆有onClick的按鈕,建置卻報事件處理函式不能傳到Client Component。先確認這份page是否仍是預設的Server Component。要在瀏覽器處理點擊與本地互動狀態,應把那一小段放進Client Component,而不是在伺服器頁直接加入一般的事件函式。
修正不一定需要把整頁都加上use client。頁面的資料讀取、文字與其他伺服器工作可以保留原處,只把計數按鈕或其他互動區域抽成客戶端元件。這樣依賴與責任比較清楚,也避免只為一顆按鈕搬動整個資料流程。
這份page預設在伺服器側處理
下面是最小問題寫法。它沒有use client,卻在原生button上帶一般onClick函式。
export default function Page() {
return <main>
<button onClick={() => console.log('click')}>計數</button>
</main>
}
函式裡的console.log本身不是重點,換成setState或其他任意click函式也沒有解決元件邊界。問題是伺服器渲染的這段UI,要把一個一般事件處理函式留給瀏覽器,卻沒有合適的Client入口。
本文在Next.js16.3.8與React19.3.0測試此page,next build --webpack在prerender階段失敗,結束碼1,訊息指出事件處理函式不能傳給Client Component props,並建議把需要互動的部分轉成Client元件。這不是單純編輯器警告,而是真建置輸出。
先建立最小Counter客戶端元件
在單獨檔案最前面加 'use client',再使用React的本地狀態與事件。指令要位於匯入之前,讓框架辨認這份檔案是Client邊界入口。
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>
Count {count}
</button>
}
Counter自行定義點擊函式,不要求伺服器把一段一般函式傳進來。這個最小例子只改本地計數,沒有呼叫API或保存資料。先讓它正常互動,再接回真正的功能,比一開始把整套訂單操作包進去更容易核對問題。
現在Server page只需匯入並放置Counter。這一份page仍可以保持Server Component。
import Counter from './counter'
export default function Page() {
return <main>
<h1>Server page</h1>
<Counter />
</main>
}
檔案名稱與匯入路徑要一致。大小寫在不同檔案系統可能有差異,測試站可用完整原名核對,不要本機能匯入就認為部署環境必然相同。這個查修只需要一個明確的元件邊界,不必引入額外狀態套件。
真點擊確認互動仍在瀏覽器執行
本文修正版使用相同Next與React版本,啟動隔離開發伺服器後,以真Chromium開啟頁面。伺服器標題是Server page,按鈕起始Count0,點擊後變Count1。這核對了最小客戶端元件確實收到互動,不是只看HTML裡有一顆button。
本例的修正版核了開發HTTP與真點擊;原錯誤則用建置失敗保存證據。它沒有被稱為正式部署或完整網站操作通過。讀者把程式放回自己的專案後,仍應執行該專案的建置檢查與頁面互動,確認其他依賴沒有一起出問題。
若按鈕只顯示卻不能互動,也要查看客戶端載入與執行錯誤。Server回HTML,不等於瀏覽器已經完成相關Client程式的執行。這是為什麼修正後除了核page內容,還要真的點一次。
不要傳任意伺服器函式當onClick
如果Server page仍建立handleClick,再把它當一般prop傳給Counter,可能又回到同樣的邊界問題。Client入口的props應符合框架要求的可序列化資料,這個本地點擊函式應在Client元件內定義。
例如把初始計數或可顯示文字傳進去,可以讓Client元件根據資料工作;把任意閉包傳過去並期待它在瀏覽器照樣執行,則不是相同的契約。原函式可能使用伺服器資源,不能因為放到prop裡就變成客戶端程式。
Server Action另有自己的介面與執行方式,不要把本例的一般onClick閉包改稱Action來繞過問題。如果真正需求是向伺服器提交修改,按那個功能的資料驗證、授權與回應流程設計;本文只處理本地互動邊界,不教一次按鈕就完成後端更新。
use client不表示所有內容只能在瀏覽器出現
Client Component仍可能參與初始頁面的伺服器預覽渲染,再在客戶端接上互動。不要把use client理解為任何程式都可以在渲染時直接讀window或localStorage。使用瀏覽器專屬API還要選適合的客戶端執行時機。
本例的Counter只用數字與useState,不讀瀏覽器儲存,也不查DOM尺寸,所以可以保持最小。若後來添加瀏覽器專屬資料,應另核初始渲染與掛載後的行為,避免修完事件錯誤後又引入另一種環境錯誤。
同樣地,use client不是授權開關。把元件移到Client,不會讓伺服器資料自動變得安全可公開;哪些資料可以傳給客戶端,仍由自己的資料層與產品需求決定。不要為了讓按鈕工作,把整份包含內部欄位的物件順便傳出去。
Client邊界盡量只包需要互動的部分
頁面上的一顆計數按鈕與整頁資料查詢,不一定需要相同執行位置。把Counter抽出,Server page仍能沿既有方式讀資料,再傳必要顯示值給Client。這個拆分能讓維護者知道哪段處理點擊,哪段負責資料來源。
並不是每個子檔案都要重複寫use client;指令定義的是入口邊界,相關匯入依賴會按框架規則處理。先把這個入口放在合適位置,再看哪些依賴真有必要進入Client,而不是把整個元件樹逐檔補上指令。
對已存在的Server頁,只為了這次互動新增一個小元件通常是容易審查的修改。保持原有資料查詢、標題與路由內容,避免順手改造其他功能。若互動區域本身需要共享狀態,再依真正需求拆分,不為簡單計數建立額外架構。
核最小成功,再接業務動作
先保存錯誤建置訊息,修正後核Server標題、Client初值與一次真點擊。接著再加入真正需要的API或表單操作,每加一項核對應的成功與失敗分支。這樣才能知道後續故障來自業務工作,還是Client邊界仍沒有修好。
不要把console.log出現當成修改已保存,也不要把Count0變1當成伺服器資料更新。互動成功與後端完成是兩條不同的證據;這份修正先讓瀏覽器能處理自己的事件,再讓真正的保存流程按既有契約執行。
如果計數的下一值依賴前一值,也可以使用React的函式更新方式 setCount(previous => previous + 1)。這讓更新條件直接表達出來,尤其同一流程安排多次更新時更容易理解。它仍只是本地狀態更新,不改變Server與Client的界線;不要把改setState寫法當成原本事件邊界錯誤的解法。先修元件位置,再按互動需求選狀態更新方式。

評論0