這篇文章要來講講如何在RSC架構下實現Next.js Server Actions,完成註冊登入功能只要10分鐘!!
原網址:Notion
RSC 是Next.js近年(2026)最推薦的架構,不僅大幅提升了DX (Developer experience),在Browser render的效能也大幅提升了,甚至Next.js Server Actions能更加隱私的完成API的連接。
RSC ( React Server Components )
shadcn-ui (Tailwind CSS)
MongoDB
src/
└── app/
└── auth/
├── page.tsx (Client Component: 負責互動事件)
└── actions.ts (Server Actions: 負責 POST/DELETE/UPDATE)
資料庫如下:
Cluster0/
└── DB/ (DataBase)
└── auth (Collection)
這裡是頁面導出的地方,因為會有互動事件所以是”use Client”
"use client"
import React, { useActionState } from "react"
import Link from "next/link"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { handleAuth } from "./actions"
export default function AuthPage() {
const [state, formAction, isPending] = useActionState(handleAuth, null)
const CommonFields = () => ( // 符合DRY原則
<>
<div className="space-y-2">
<Label>Email</Label>
<Input name="email" type="email" placeholder="you@example.com" required />
</div>
<div className="space-y-2">
<Label>Password</Label>
<Input name="password" type="password" required />
</div>
</>
)
return (
<div className="flex min-h-screen items-center justify-center bg-background px-4">
<div className="w-full max-w-md">
<Card className="bg-card/90 backdrop-blur-sm shadow-sm border-border">
<CardHeader className="space-y-2 text-center">
<CardTitle className="text-xl font-semibold text-foreground">Welcome</CardTitle>
<CardDescription className="text-sm text-muted-foreground">登入或註冊!</CardDescription>
</CardHeader>
<CardContent>
{state?.error && <div className="p-2 mb-4 text-sm text-red-500 bg-red-50 rounded border border-red-200">{state.error}</div>}
{state?.success && <div className="p-2 mb-4 text-sm text-green-500 bg-green-50 rounded border border-green-200">{state.success}</div>}
<Tabs defaultValue="login" className="w-full">
<TabsList className="grid w-full grid-cols-2 bg-muted/60">
<TabsTrigger value="login">登入</TabsTrigger>
<TabsTrigger value="register">註冊</TabsTrigger>
</TabsList>
{/* 登入表單 */}
<TabsContent value="login">
<form action={formAction} className="mt-6 space-y-4">
<input type="hidden" name="auth-type" value="login" />
<CommonFields/>
<Button className="w-full mt-2" disabled={isPending}>{isPending ? "處理中..." : "登入"}</Button>
</form>
</TabsContent>
{/* 註冊表單 */}
<TabsContent value="register">
<form action={formAction} className="mt-6 space-y-4">
<input type="hidden" name="auth-type" value="register" />
<div className="space-y-2">
<Label htmlFor="name">Name</Label>
<Input id="name" name="name" placeholder="Jane Doe" required />
</div>
<div className="space-y-2">
<Label htmlFor="role">Role (身分)</Label>
<Select name="role" defaultValue="DEV">
<SelectTrigger>
<SelectValue placeholder="選擇你的角色" />
</SelectTrigger>
<SelectContent>
<SelectItem value="PO">Product Owner (PO)</SelectItem>
<SelectItem value="DEV">Developer (DEV)</SelectItem>
<SelectItem value="TL">Team Lead (TL)</SelectItem>
</SelectContent>
</Select>
</div>
<CommonFields/>
<Button className="w-full mt-2" disabled={isPending}>{isPending ? "建立帳號中..." : "建立帳號"}</Button>
</form>
</TabsContent>
</Tabs>
<p className="mt-6 text-center text-xs text-muted-foreground">
@2026 Hy.Chen
</p>
</CardContent>
</Card>
</div>
</div>
)
}
是 React 19推出的表單管理hook
https://zh-hans.react.dev/reference/react/useActionState
const [state, formAction, isPending] = useActionState(handleAuth, null)
| 變數名稱 | 名稱 | 做什麼用的? |
|---|---|---|
state |
Action 狀態 | 這是後端回傳的結果(例如:{ error: "密碼錯誤" })。它讓你能在前端畫面顯示「註冊成功」或「報錯」。 |
formAction |
表單動作 | 你要把它放在 <form action={formAction}>。當用戶點擊按鈕,它會自動把整份表單資料丟給後端。 |
isPending |
處理狀態 | 一個 布林值 (true/false)。當資料正在傳輸時,它是 true。這讓你能在按鈕上顯示「轉圈圈」或變更文字。 |
取代了傳統寫法:
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [data, setData] = useState(null);
複雜互動:Tabs & Select
<select> 在不同瀏覽器(Chrome vs Safari)樣式難以統一的痛點,同時支援漂亮的動畫效果。shadcn/ui 的核心價值在於它遵循了 Composition Pattern (組合模式),這讓我們的
AuthPage即使邏輯複雜,程式碼結構依然清晰可讀,且具備完整的無障礙支持。
.env.local
# .env.local
MONGODB_URI=mongodb+srv://<username>:<password>@cluster.mongodb.net/Agile-ESS?retryWrites=true&w=majority
bcryptjs :一個專門為密碼雜湊Hashing設計的函式庫
npm install bcrypt
npm install --save-dev @types/bcrypt
mongodb
npm install mongodb
"use server"
import { MongoClient } from "mongodb"
import bcrypt from "bcrypt"
import { redirect } from "next/navigation"
const client = new MongoClient(process.env.MONGODB_URI!)
export async function handleAuth(prevState: any, formData: FormData) {
const type = formData.get("auth-type") // 判斷是 login 還是 register
const email = formData.get("email") as string
const password = formData.get("password") as string
await client.connect()
const db = client.db("ESS-db")
const collection = db.collection("user")
if (type === "register") {
const name = formData.get("name") as string
const role = formData.get("role") as string
// 檢查重複
const existing = await collection.findOne({ email })
if (existing) return { error: "該 Email 已被註冊" }
// 加密並儲存
const hashedPassword = await bcrypt.hash(password, 10)
await collection.insertOne({
name,
email,
password: hashedPassword,
role,
createdAt: new Date()
})
return { success: "註冊成功!請切換至登入分頁" }
}
if (type === "login") {
const user = await collection.findOne({ email })
if (!user) return { error: "用戶不存在" }
const isMatch = await bcrypt.compare(password, user.password)
if (!isMatch) return { error: "密碼錯誤" }
// 這裡通常會設定 Cookie/Session,目前先模擬成功導向
redirect("/dashboard")
}
}
prevState: 因為我們前端使用了 useActionState,所以第一個參數必須是「上一次的狀態」。雖然這裡沒用到,但位置必須留著。formData.get(...): 這是原生 Web API。Server Actions 最優雅的地方在於它直接處理 FormData 物件,你不需要像以前一樣寫 req.body.email。as string: 強制轉型,因為 get 拿到的可能是檔案或字串,我們明確告知這是文字。connect() (連接) -> db() (選擇資料庫) -> collection() (選擇資料表)。用Server Actions的寫法實現了最"整潔”的寫法:
/api/login。"use server" 牆後。