在 Next.js App Router 中,Client Component 不能直接匯入 Server Component;當你想把像「搜尋輸入」這種即時變動的客戶端狀態傳回 RSC 時,最實用的做法之一是把狀態寫進 URL 的 search params,並讓 page 透過 searchParams 取得值後再把它傳給 RSC。Next.js 也建議在 Server Component Page 使用 searchParams prop、在 Client 端用 useSearchParams/useRouter 更新查詢字串來觸發頁面拿到最新參數;若你想要更好用的 query string 狀態管理體驗,也可以搭配 nuqs。
Written by: Chia1104 CC BY-NC-SA 4.0
這一陣子的專案開發多半離不開 Next.js 的使用,其中 RSC 跟一般 Client Compnent 的交互使用也越來越頻繁。
其中有一個情境是當我在客戶端有個狀態會動態更新,例如使用者的搜尋 input,而這狀態需要更新到 RSC 的 props 中,而這時可以怎麼做呢?
React Server Components (RSC) 就是 在伺服器上執行的 React 元件,用來減少送到瀏覽器的 JavaScript,讓載入更快、體驗更順。
在 Next.js 使用 App Router 下 (app/),page 和 layout 預設就是 Server Components,只有標記 "use client" 的檔案才會變成 Client Components。
這裡補充一下另一個指令 (Directives) - "use server",這指令是把該模組或方法標示成建置時需要在伺服器端生成的 Server Functions 而非 RSC,過去看過有人把這當成匯入 RSC 的依據 但這是錯誤的。
若是在元件中寫這指令並寫在 render 方法中是會觸發 infinite loop 的!
"use server";
const DDoSMyself = async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return <p>I'm a Server Function</p>
}import DDoSMySelf from "./DDoSMySelf";
const SharedComponent = () => {
return <div>
I'm a Shared Component
<DDoSMySelf /> /* [!code --] */
</div>
}由於 Server Component 會有不少跟伺服器端的互動,所以 RSC 在使用上是有一些明確規定的用法的。若是要避免這些元件或方法在錯誤的地方被引用的話可以用 React team 所做的這兩個 package:server-only 跟 client-only。
server-only:用於 Next.js 等框架中,以確保特定程式碼僅在 伺服器端運行,如果意外地將其匯入到客戶端程式碼中,則會拋出 建置時錯誤。client-only:用於 Next.js 等框架中,以確保特定程式碼僅在 客戶端運行,如果意外地將其匯入到伺服器端程式碼中,則會拋出 建置時錯誤。useState、useReducer、useEffect 等都是不允許的。window、document、localStorage 等任何只在瀏覽器存取的東西都不能用。onClick、onChange 是不被允許的,需要把那一塊切成 Client Component。children 的形式去 包 Server Component。前面稍微說明一下 RSC 的使用規範,有一點是 Client Component 無法匯入 Server Component。
那如果我們有一個課會端的狀態要跟新到 Server component 該如何做?
比如說我有一個搜尋的 input,這裡的狀態會去搜尋 DB 資料
"use client";
import { useState } from "react";
export const SearchAction = () => {
const [search, setSearch] = useState("");
return (
<>
<input value={search} onChange={(e) => setSearch(e.target.value)} />
</>
);
};import "server-only";
const sql = async (args: TemplateStringsArray, ...values: any[]) => {
const query = args.map((arg, i) => arg + (values[i] || "")).join("");
await new Promise((resolve) => setTimeout(resolve, 1000));
return [];
};
export const ServerDataGrid = async ({ search }: { search: string }) => {
const result = await sql`SELECT * FROM users WHERE name LIKE '%${search}%'`;
return (
<table>
{result.map((row) => (
<tr key={row.id}>{row.name}</tr>
))}
</table>
);
};依照先前的規範,我們是無法在 SearchAction 直接 import ServerDataGrid 做使用的。
"use client";
import { useState } from "react";
import { ServerDataGrid } from "./ServerDataGrid";
export const SearchAction = () => {
const [search, setSearch] = useState("");
return (
<>
<input value={search} onChange={(e) => setSearch(e.target.value)} />
<ServerDataGrid search={search} /> /* [!code --] */
</>
);
};這時 React 就會透過 server-only 拋出這錯誤

You're importing a component that needs "server-only". That only works in a Server Component but one of its parents is marked with "use client", so it's a Client Component.
Learn more: https://nextjs.org/docs/app/building-your-application/rendering前面有說 RSC 的生成是在伺服器端生成元件後再以 RSC Payload 的格式 streaming 到客戶端,這意味著我們今天要取得 RSC 一定是透過一個 fetch 的方式取得

所以對於 React server 他也需要一個狀態來取得,意味著在伺服器端只要能接收到這些從客戶端發出的 incoming request 都可以,例如 Search Params 或 next/headers 的 headers 跟 cookies 都可以。
我們把剛剛的範例改一下:
"use client";
import { useSearchParams } from "next/navigation";
import { useRouter } from "next/navigation";
export const SearchAction = ({ children }: { children: React.ReactNode }) => {
const searchParams = useSearchParams();
const router = useRouter();
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
const params = new URLSearchParams(searchParams.toString());
params.set("search", e.target.value);
router.push(`?${params.toString()}`);
};
return (
<>
<input value={searchParams.get("search") || ""} onChange={handleSearch} />
{children}
</>
);
};這時候我們改在最外層的 Server Component 做 import
import { SearchAction } from "./search-action";
import { ServerDataGrid } from "./server-data-grid";
import { Suspense } from "react";
export default async function Page({ searchParams }: { searchParams: Promise<{ [key: string]: string }> }) {
const { search } = await searchParams;
return (
<SearchAction>
<Suspense fallback={<div>Loading server data-grid...</div>}>
<ServerDataGrid search={search} />
</Suspense>
</SearchAction>
);
}這樣就可以正常在 RSC 取得客戶端的狀態了。
另外要對 Search Params 做互動自己蠻推薦 nuqs 的,一個專門針對 Search Params 做狀態管理的套件,並可透過 parser 做驗證跟轉換,在 client side 跟 server side 都可做使用
