如何在客戶端更新 RSC (React Server Component) 的狀態

Client Component 無法直接匯入 Server Component。把搜尋等即時狀態寫進 URL search params,讓頁面透過 searchParams 取值再傳給 RSC,並可搭配 nuqs 管理。

CChia1104
  • 文章
  • 8 分鐘閱讀

這一陣子的專案開發多半離不開 Next.js 的使用,其中 RSC 跟一般 Client Compnent 的交互使用也越來越頻繁。

其中有一個情境是當我在客戶端有個狀態會動態更新,例如使用者的搜尋 input,而這狀態需要更新到 RSC 的 props 中,而這時可以怎麼做呢?

什麼是 React Server Component (RSC)

React Server Components (RSC) 就是 在伺服器上執行的 React 元件,用來減少送到瀏覽器的 JavaScript,讓載入更快、體驗更順。

  • RSC 讓一部分 React 元件只在伺服器跑,不會被打包到瀏覽器,因此不會出現在 client bundle 裡。
  • 這些 Server Components 把畫面先算好,產生一份特殊格式的資料 (RSC payload),再傳給瀏覽器用來組成 HTML。

在 Next.js 使用 App Router 下 (app/),page 和 layout 預設就是 Server Components,只有標記 "use client" 的檔案才會變成 Client Components。

這裡補充一下另一個指令 (Directives) - "use server",這指令是把該模組或方法標示成建置時需要在伺服器端生成的 Server Functions 而非 RSC,過去看過有人把這當成匯入 RSC 的依據 但這是錯誤的。

若是在元件中寫這指令並寫在 render 方法中是會觸發 infinite loop 的!

DDoSMyself.tsx
"use server"; 

const DDoSMyself = async () => {
  await new Promise((resolve) => setTimeout(resolve, 1000));
  return <p>I'm a Server Function</p>
}
SharedComponent.tsx
import DDoSMySelf from "./DDoSMySelf"; 

const SharedComponent = () => {
  return <div>
    I'm a Shared Component
    <DDoSMySelf /> /* [!code --] */
  </div>
}

RSC 使用規則

由於 Server Component 會有不少跟伺服器端的互動,所以 RSC 在使用上是有一些明確規定的用法的。若是要避免這些元件或方法在錯誤的地方被引用的話可以用 React team 所做的這兩個 package:server-only 跟 client-only。

  • server-only:用於 Next.js 等框架中,以確保特定程式碼僅在 伺服器端運行,如果意外地將其匯入到客戶端程式碼中,則會拋出 建置時錯誤。
  • client-only:用於 Next.js 等框架中,以確保特定程式碼僅在 客戶端運行,如果意外地將其匯入到伺服器端程式碼中,則會拋出 建置時錯誤。

Server Component 不能做的事

  • 不能用 React hooks 做本地狀態或副作用:例如 useState、useReducer、useEffect 等都是不允許的。
  • 不能用瀏覽器 API:像 window、document、localStorage 等任何只在瀏覽器存取的東西都不能用。
  • 不能直接掛事件處理:例如在 Server Component 裡下 onClick、onChange 是不被允許的,需要把那一塊切成 Client Component。

Server Component 可以做的事

  • 可以是非同步的元件,例如在 render 階段直接 await DB 查詢或呼叫後端跟外部 API,很適合用來做資料抓取和組裝 UI。
  • 可以安全地存取 只在 server 有的東西,例如檔案系統、私密金鑰、環境變數、後端 SDK 等,不會被送到瀏覽器。
  • 可以把資料以可序列化 (serializable) 的 props 傳給 Client Components,例如字串、數字、物件、陣列、JSON 等,但不能傳 function、class instance、Date 等非序列化值。

Client / Server 互相引用的規則

  • Client Component 只能匯入 Client Component,不能直接 import Server Component,否則這檔案 會被視為 client bundle 被匯入。
  • 反過來,Server Component 可以匯入 Client Component,然後用 JSX 去 包 它,把資料以 props 傳進去,形成 外層 server、內層 client 的結構。
  • 同理若是要在 Client Component 中使用 Server Component,也必須在 Client Component 中以 children 的形式去 包 Server Component。

RSC 狀態更新

前面稍微說明一下 RSC 的使用規範,有一點是 Client Component 無法匯入 Server Component。

那如果我們有一個課會端的狀態要跟新到 Server component 該如何做?

比如說我有一個搜尋的 input,這裡的狀態會去搜尋 DB 資料

SearchAction.tsx
"use client";

import { useState } from "react";

export const SearchAction = () => {
  const [search, setSearch] = useState("");

  return (
    <>
      <input value={search} onChange={(e) => setSearch(e.target.value)} />
    </>
  );
};
ServerDataGrid.tsx
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 做使用的。

SearchAction.tsx
"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 拋出這錯誤

server only error

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

透過 Search Params 更新狀態

前面有說 RSC 的生成是在伺服器端生成元件後再以 RSC Payload 的格式 streaming 到客戶端,這意味著我們今天要取得 RSC 一定是透過一個 fetch 的方式取得

fetch rsc

所以對於 React server 他也需要一個狀態來取得,意味著在伺服器端只要能接收到這些從客戶端發出的 incoming request 都可以,例如 Search Params 或 next/headers 的 headers 跟 cookies 都可以。

我們把剛剛的範例改一下:

SearchAction.tsx
"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

app/page.tsx
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 都可做使用

nuqs

最後更新

Written by: Chia1104 CC BY-NC-SA 4.0