🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

隨著對 React / Next.js 的熟悉,接下來通常會煩惱的就是「設計層」的問題了。

  • 組件容易肥胖
  • useEffect 的增加讓邏輯變得迷惘
  • 每次都在猶豫狀態放在哪裡
  • 不知道 Hooks 要分割到什麼程度

若能事先將這些困惑語言化,

  • 代碼的可讀性
  • 修改時的“恐懼感”降低
  • 團隊開發時的壓力

都會有很大的變化。

本文將基於 2024〜2025 年的 React 19 / Next.js 15(App Router) 的設計思想,
整理出 為了不在實務中迷惘的 “判斷軸”,包括例外和補充。

※ Next.js 15 已被官方推薦與 React 19 組合使用,但
本文介紹的思考方式也能應用於基於 React 18 的現有專案。


1. 設計從「UI 的變化」逆推會輕鬆很多

雖然很想從 API 或 DB 結構開始思考,但用戶實際接觸的是 UI 的變化 本身。

  • 輸入了什麼
  • 哪個 UI
  • 在什麼時候
  • 如何變化

只需將這些 UI 的狀態遷移 簡單寫出來,

  • 需要的 state
  • 組件的劃分
  • Hooks 的切割方式

就會變得相當自然。

但有些情況下不以 UI 為起點反而是更安全的

作為例外,像以下情況時,從領域邏輯為起點 的設計更為安全。

  • 在金額計算、庫存、積分等,整合性為最優先的 EC / 業務 UI
  • 系統的業務模型或表格結構已經十分確定的情況
  • 有著「這個值的整合性絕對不能崩壞」等強制性限制的情況

這些情況在專案中若能提前決定 「UI優先」還是「領域優先」,會降低團隊內部的認知偏差。

❌ 常見的混淆案例(UI狀態和領域狀態混淆)

const UserForm = () => {
  const [user, setUser] = useState({ name: "", age: 0 });
  const [isLoading, setIsLoading] = useState(false);
  const [isSuccess, setIsSuccess] = useState(false);

  // ...
};

這段代碼看似簡單,但

  • user 是“當前輸入的值”還是
  • 經過驗證的“作為領域的正確值”還是
  • 直接投入 API 回應的值

都很模糊,逐漸會讓人無法分辨 「應該信任的 state 是什麼」

將 UI 狀態和輸入狀態分開

const [name, setName] = useState("");
const [ageInput, setAgeInput] = useState("");
const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");

然後 只有在提交時轉換為領域類型

const handleSubmit = async () => {
  const age = Number(ageInput);
  if (!Number.isInteger(age) || age < 0) {
    setStatus("error");
    return;
  }

  setStatus("loading");

  await updateUser({ name, age });

  setStatus("success");
};
  • 輸入值以 「UI 易於處理的類型」(如字串)保存
  • 直到傳送到服務器前,再轉換為 「作為領域的合理類型」

這樣的流程即使表單日益複雜,也能降低崩潰的風險。

React 19 時代的表單設計要點

在 React 19 中,

  • <form>Actions(useActionState / useFormStatus 等)
  • 原生表單提交和服務器動作

得到了顯著的加強。

這意味著,設計的選項大致只有這兩個。

  • 將所有狀態作為 UI 狀態在 state 中持有
  • 任由 DOM(表單)處理,只有動作結果作為狀態持有

若表單數量逐漸增加,
只需靜下心來思考 「這應該用 React 的 state 嗎? / 還是交給 DOM?」
就能更容易從充斥著 useState 的表單中畢業。


2. 狀態設計依賴「三個軸 + URL 狀態 + 全球狀態」不會偏離

在狀態設計中容易迷失的要點,可以分解為以下三步。

  1. 確定狀態的 “擁有者”
  2. 對狀態的 類型 進行分類
  3. 不要過多增加 Derived State(派生狀態)

再加上 URL 狀態全球狀態,能大幅減少偏差。

(1) 確定狀態的 “擁有者”

首先簡單考慮這兩點。

  • 哪個組件最自然地處理這個 state
  • 使用這個 state 的組件的 「最小共通父組件」 是哪個?

僅通過這兩點,

  • 「暫時將所有狀態放在父組件中,props 鑽取…」
  • 「因為想從任何地方觸及,所以全球化…」

這樣的 “逃避配置” 會極大減少。

(2) 根據狀態類型進行分類

大致而言,在實務中將狀態劃分為以下幾類會更容易理清思緒。

種類 例子
UI 狀態 模態框、標籤、加載、提示
伺服器狀態 API 數據、快取、重新驗證
派生狀態(Derived) 篩選結果、彙總、選中標誌等
表單狀態 輸入值、錯誤、驗證
URL / 路由狀態 搜尋條件、頁碼、排序條件
全球 / 會話狀態 登入用戶、權限、主題

在 Next.js 15(App Router)中,特別是

  • fetch + 快取(revalidate
  • 伺服器組件 / 伺服器動作

都相當強大,因此可以將 「伺服器狀態 = 不僅僅是 DB 的值,還包括快取的“讀取窗口”」 來設計,會更加方便。

(3) 避免「沒有目的的重複」的派生狀態

// ❌ 沒有目的的重複
const [filteredUsers, setFilteredUsers] = useState<User[]>([]);

// ...
setFilteredUsers(users.filter((u) => u.active));

如果將這種「原數據的單純篩選結果」設為 state,

  • usersfilteredUsers 之間的正確性會產生混淆
  • 也容易導致更新遺漏的錯誤

因此更安全的選擇是:

// 計算只要足夠就不需要 state
const filteredUsers = users.filter((u) => u.active);

若能僅此解決,首先 最好不使用 state

但在以下情況下,使用 state 會更直觀:

  • 計算成本較高(且想進行快取)
  • 想與無限捲動或分頁強烈同步
  • 在度量方面,需要將“當前顯示數量”作為 state 保存

關於 useMemo 也要注意,

  • 不要單純因為需要加速而添加
  • 應該只在 需要保證參照穩定性(=== 的變化) 的地方添加

這樣可以防止不必要的增長。


3. 組件分割依據「責任 + 再渲染邊界」

最早的 Presentational / Container 的理念至今仍然非常有效。

// View(僅負責 UI)
export const UserListView = ({ users }: { users: User[] }) => (
  <ul>
    {users.map((u) => (
      <li key={u.id}>{u.name}</li>
    ))}
  </ul>
);

// Logic(數據獲取 + 篩選)
export const UserList = () => {
  const users = useUsers();
  const filtered = useFilteredUsers(users);

  return <UserListView users={filtered} />;
};

但並不是說「一定要分為 View 和 Container」。
事實上,在實務上,根據接下來的兩個軸來判斷會更合理。

  1. 責任

    • 想要重用的 UI → 提取為組件
    • 想要重用的邏輯 → 提取為 Hook
    • 頁面特有邏輯 → 保留在頁面內
    • 業務邏輯 → 向 use-case / service 層靠攏
  2. 再渲染邊界

    • 「如果在這裡持有 state,下面的組件樹都會重新渲染,可以接受嗎?」
    • 「在這裡適當地分開,能縮小再渲染範圍嗎?」

通過這兩個軸來觀察,能夠更容易避免:

  • 不必要的產生太多組件
  • 反之,形成一個長達 500 行的“萬能組件”

的兩極狀態。


4. 副作用“必要時逃避至 Hook”

useEffect 相關的問題,在 React 19 中仍然是個陷阱。

大致分為以下的方針建議。

  • 想重用的副作用 → 提取為自定義 Hook
  • 頁面專用的副作用 → 不必強行提取,保留在頁面內也可以

不過,有些「想要妥善測試的副作用」更適合逃避到 Hook 中,後期會更輕鬆。

  • 日誌發送 / 追蹤
  • API 調用(輪詢、計時器、觀察者)
  • 事件訂閱(IntersectionObserver / WebSocket / SSE 等)

❌ 常見的 useEffect “萬用存放處”

隨著 React 19 的渲染模型變得更豐富,
“隨便放進 useEffect 中” 的做法變得越來越危險。

尤其應避免如下情況:

  • 將單純計算封閉於 useEffect
  • 將 URL(查詢)同步至單獨的 state 中
  • 將表單的本地值寫入至單獨的 state 中

這些行為會導致

  • render → effect → state 更新 → render → ...

的回圈,更容易造成錯誤及性能問題。

在編碼時習慣性地思考「是否可以用純計算的方式來寫?」,就能大大減少 useEffect 的使用次數。


5. Next.js 的數據獲取應首先考慮「Server Component」(但不是萬能)

在 App Router 的世界觀中,

  • 初始展示的數據獲取應由 Server Component 處理
  • 在此基礎上,真的需要保留在客戶端的部分才轉由 Client Component

這樣的流程是基本的設計思路。

初始展示數據由 Server Component 處理的優勢

// app/users/[id]/page.tsx
export default async function Page({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const user = await getUser(id);

  return <UserPage user={user} />;
}

通過 Server Component 獲取數據的優勢大致如下。

  • 自動快取(與 revalidate 策略的組合)
  • 自動重新驗證(Stale-While-Revalidate)
  • 改善 TTFB(不需要客戶端的 useEffect fetch)
  • 更容易減少初始展示的「閃爍」

⚠ 然而,也有很多情況無法僅用 Server Component

例如,以下情況更直接在客戶端進行 fetch 會比較好。

  • UI 根據認證 / Cookie / 權限多次變化
  • 外部 API,難以進行快取策略(如只需一次的有效載荷)
  • 用戶的個性化數據需要强烈的交互(高實時性場景)

並不是所有東西都向 Server Component 傾斜,

  • 將“初始顯示所需的最小限度”交由 Server Component
  • 隨後的微交互或輪詢則轉由 Client 負責

這樣的平衡能使設計更加易於理解。


6. 更新以 Server Actions 方式進行將會十分強大(但也需理解其約束)

在 Next.js 15 中,Server Actions 已經穩定,
「從表單到 DB 的更新可以用一條線來實現」的風格變得非常實用。

// app/users/actions.ts
"use server";

export async function updateUser(
  prevState: { ok: boolean },
  formData: FormData
) {
  const name = formData.get("name");
  await db.user.update({ name });

  return { ok: true };
}
// app/users/page.tsx(Client Component 方面)
"use client";

import { useActionState } from "react";
import { updateUser } from "./actions";

export function UserForm() {
  const [state, action, isPending] = useActionState(updateUser, { ok: false });

  return (
    <form action={action}>
      <input name="name" />
      <button disabled={isPending}>更新</button>
      {state.ok && <p>更新成功</p>}
    </form>
  );
}

Server Actions 的優勢

  • pending / error / success 能用 useActionState / useFormStatus 輕鬆處理
  • Optimistic UI 寫起來更容易
  • 能安全使用 cookie 或會話進行 mutate
  • 與原生表單相容性好,在 JavaScript 未加載的情況下仍可運行(進步增強)

但同時也有相應的約束

  • Action 含於伺服器包中,若過於龐大會影響建置大小
  • 運行時基本上是 Node.js(在邊緣運行中有許多限制和注意事項)
  • 認為基本上是 1 Action = 1 請求 的概念
  • 若想經由移動應用和 API 進行深入的共享,
    最好是另行準備 BFF 或 OpenAPI 基礎的 API

簡單的使用區分想法

  • Server Actions

    • Web 應用中表單提交
    • 涉及權限檢查的安全 mutate
    • 強烈綁定於畫面的更新(UI 一體化)
  • Route Handlers(BFF 類似 API)

    • 與移動應用或其他服務的 API 共享
    • 希望從 Web 以外的客戶端請求的更新類流程
    • SSE / WebSocket 等高實時性系統
  • Client fetch(客戶端請求 / SWR / TanStack Query 等)

    • 想細緻控制客戶端的快取策略
    • 更新頻繁,UI 交互變化的場景

7. 將判斷軸語言化能減少迷惘

將以上內容總結為「在困惑時能想起的檢查清單」,大致如下。

  • 從 UI 的變化進行設計

    • 寫下「做了什麼,哪個 UI 變了?」再決定 state
  • 確定狀態的“擁有者”

    • 了解那個 state 最自然的處理組件和最小共通父組件
  • 根據狀態類型進行分類(考慮 URL / 全球狀態)

    • UI / 伺服器 / Derived / 表單 / URL / 全球
  • 避免輕易重複的派生狀態

    • 計算能解決的就不使用 state
    • 然而為了計測或性能的「有意義的冗餘」是可以接受的
  • 根據「責任 + 再渲染邊界」劃分組件

    • 想重用的 UI 提取為組件
    • 想重用的邏輯提取為 Hook
    • 頁面特有的邏輯保留於頁面
  • 副作用在必要時逃避至 Hook

    • 特別是需要測試的副作用(日誌、輪詢、觀察者)應提取為 Hook
  • 初始數據應優先考慮 Server Component

    • 仍然不足的部分再由 Client 進行 fetch
  • 針對 mutation 根據用途選擇手段

    • 根據「誰來請求」「希望共享到何處」選擇 Server Actions / Route Handlers / Client fetch
  • 不僅僅停留在書面的設計,而是以「測量」進行再評估

    • LCP / TTFB / INP 等性能指標
    • 通過 Sentry 或各種追蹤記錄的錯誤及行為

結語

2024〜2025 年,因 React 19 和 Next.js 15 的誕生,

  • UI 與邏輯的責任分離(Server / Client 的角色分配)
  • 「初始展示由伺服器主導」幾乎成為默認
  • 將 state 限縮到最小化,「單一真理來源」的流行
  • mutation 策略(Server Actions / BFF / Client fetch)的選項得到了整理

這一時期前端的設計思想得到了顯著的升級。

然而,「正確的設計」本身仍然因專案而異,這也是事實。

因此,

  • 從 UI 開始梳理狀態遷移
  • 將狀態的擁有者與類型語言化
  • 在團隊中分享 Server / Client 的角色分配
  • 觀察實際的測量結果來更新設計

以上都可以作為 “判斷軸” 共享在團隊中
最終穩定組件結構和代碼質量。

當有人在設計上遇到困難時,

「不如依照這個檢查清單一起整理吧」

能夠如此說的 共同基礎 若能成為參考,將會是我所期待的。


原文出處:https://qiita.com/quniquni/items/1e6bafbc6bfce74afe5d


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝15   💬3   ❤️3
315
🥈
我愛JS
📝1   💬3   ❤️2
45
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付