隨著對 React / Next.js 的熟悉,接下來通常會煩惱的就是「設計層」的問題了。
useEffect 的增加讓邏輯變得迷惘若能事先將這些困惑語言化,
都會有很大的變化。
本文將基於 2024〜2025 年的 React 19 / Next.js 15(App Router) 的設計思想,
整理出 為了不在實務中迷惘的 “判斷軸”,包括例外和補充。
※ Next.js 15 已被官方推薦與 React 19 組合使用,但
本文介紹的思考方式也能應用於基於 React 18 的現有專案。
雖然很想從 API 或 DB 結構開始思考,但用戶實際接觸的是 UI 的變化 本身。
只需將這些 UI 的狀態遷移 簡單寫出來,
state就會變得相當自然。
作為例外,像以下情況時,從領域邏輯為起點 的設計更為安全。
這些情況在專案中若能提前決定 「UI優先」還是「領域優先」,會降低團隊內部的認知偏差。
const UserForm = () => {
const [user, setUser] = useState({ name: "", age: 0 });
const [isLoading, setIsLoading] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
// ...
};
這段代碼看似簡單,但
user 是“當前輸入的值”還是都很模糊,逐漸會讓人無法分辨 「應該信任的 state 是什麼」。
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");
};
這樣的流程即使表單日益複雜,也能降低崩潰的風險。
在 React 19 中,
<form> 和 Actions(useActionState / useFormStatus 等)得到了顯著的加強。
這意味著,設計的選項大致只有這兩個。
若表單數量逐漸增加,
只需靜下心來思考 「這應該用 React 的 state 嗎? / 還是交給 DOM?」,
就能更容易從充斥著 useState 的表單中畢業。
在狀態設計中容易迷失的要點,可以分解為以下三步。
再加上 URL 狀態 和 全球狀態,能大幅減少偏差。
首先簡單考慮這兩點。
state?state 的組件的 「最小共通父組件」 是哪個?僅通過這兩點,
這樣的 “逃避配置” 會極大減少。
大致而言,在實務中將狀態劃分為以下幾類會更容易理清思緒。
| 種類 | 例子 |
|---|---|
| UI 狀態 | 模態框、標籤、加載、提示 |
| 伺服器狀態 | API 數據、快取、重新驗證 |
| 派生狀態(Derived) | 篩選結果、彙總、選中標誌等 |
| 表單狀態 | 輸入值、錯誤、驗證 |
| URL / 路由狀態 | 搜尋條件、頁碼、排序條件 |
| 全球 / 會話狀態 | 登入用戶、權限、主題 |
在 Next.js 15(App Router)中,特別是
fetch + 快取(revalidate)都相當強大,因此可以將 「伺服器狀態 = 不僅僅是 DB 的值,還包括快取的“讀取窗口”」 來設計,會更加方便。
// ❌ 沒有目的的重複
const [filteredUsers, setFilteredUsers] = useState<User[]>([]);
// ...
setFilteredUsers(users.filter((u) => u.active));
如果將這種「原數據的單純篩選結果」設為 state,
users 和 filteredUsers 之間的正確性會產生混淆因此更安全的選擇是:
// 計算只要足夠就不需要 state
const filteredUsers = users.filter((u) => u.active);
若能僅此解決,首先 最好不使用 state。
但在以下情況下,使用 state 會更直觀:
關於 useMemo 也要注意,
=== 的變化) 的地方添加這樣可以防止不必要的增長。
最早的 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」。
事實上,在實務上,根據接下來的兩個軸來判斷會更合理。
責任
再渲染邊界
通過這兩個軸來觀察,能夠更容易避免:
的兩極狀態。
useEffect 相關的問題,在 React 19 中仍然是個陷阱。
大致分為以下的方針建議。
不過,有些「想要妥善測試的副作用」更適合逃避到 Hook 中,後期會更輕鬆。
IntersectionObserver / WebSocket / SSE 等)隨著 React 19 的渲染模型變得更豐富,
“隨便放進 useEffect 中” 的做法變得越來越危險。
尤其應避免如下情況:
useEffect 中這些行為會導致
render → effect → state 更新 → render → ...的回圈,更容易造成錯誤及性能問題。
在編碼時習慣性地思考「是否可以用純計算的方式來寫?」,就能大大減少 useEffect 的使用次數。
在 App Router 的世界觀中,
這樣的流程是基本的設計思路。
// 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 策略的組合)useEffect fetch)例如,以下情況更直接在客戶端進行 fetch 會比較好。
並不是所有東西都向 Server Component 傾斜,
這樣的平衡能使設計更加易於理解。
在 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>
);
}
pending / error / success 能用 useActionState / useFormStatus 輕鬆處理Server Actions
Route Handlers(BFF 類似 API)
Client fetch(客戶端請求 / SWR / TanStack Query 等)
將以上內容總結為「在困惑時能想起的檢查清單」,大致如下。
從 UI 的變化進行設計
確定狀態的“擁有者”
根據狀態類型進行分類(考慮 URL / 全球狀態)
避免輕易重複的派生狀態
根據「責任 + 再渲染邊界」劃分組件
副作用在必要時逃避至 Hook
初始數據應優先考慮 Server Component
針對 mutation 根據用途選擇手段
不僅僅停留在書面的設計,而是以「測量」進行再評估
LCP / TTFB / INP 等性能指標Sentry 或各種追蹤記錄的錯誤及行為2024〜2025 年,因 React 19 和 Next.js 15 的誕生,
這一時期前端的設計思想得到了顯著的升級。
然而,「正確的設計」本身仍然因專案而異,這也是事實。
因此,
以上都可以作為 “判斷軸” 共享在團隊中,
最終穩定組件結構和代碼質量。
當有人在設計上遇到困難時,
「不如依照這個檢查清單一起整理吧」
能夠如此說的 共同基礎 若能成為參考,將會是我所期待的。