Chuyển đến nội dung chính

第 11 課:伺服器操作與變更

伺服器操作,「使用伺服器」。形成行動,逐步增強。樂觀更新,使用ActionState。錯誤處理、突變後重新驗證。

💻 程式設計 — 第 11 課 第 11 課:伺服器操作與變更

React 和 Next.js:從基礎到高級

第 3 部分:Next.js 基礎知識

亞洲開發網

1. 什麼是伺服器操作?

伺服器操作是在伺服器上執行的非同步函數,透過表單或事件處理程序從客戶端呼叫。替換突變的 API 路由。

// actions.ts
'use server';

import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { db } from '@/lib/db';

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  await db.post.create({
    data: { title, content },
  });

  revalidatePath('/posts');
  redirect('/posts');
}

2. 表單操作

// Server Component — no JS needed on client
import { createPost } from './actions';

export default function NewPostPage() {
  return (
    <form action={createPost}>
      <input name="title" required placeholder="Tiêu đề" />
      <textarea name="content" required placeholder="Nội dung" />
      <button type="submit">Tạo bài viết</button>
    </form>
  );
}

3. 使用動作狀態

'use client';
import { useActionState } from 'react';
import { createPost } from './actions';

type State = {
  errors?: { title?: string; content?: string };
  message?: string;
};

export function PostForm() {
  const [state, formAction, isPending] = useActionState<State, FormData>(
    createPost,
    { errors: {}, message: '' }
  );

  return (
    <form action={formAction}>
      <input name="title" />
      {state.errors?.title && <p className="text-red-500">{state.errors.title}</p>}

      <textarea name="content" />
      {state.errors?.content && <p className="text-red-500">{state.errors.content}</p>}

      <button type="submit" disabled={isPending}>
        {isPending ? 'Đang tạo...' : 'Tạo bài viết'}
      </button>

      {state.message && <p>{state.message}</p>}
    </form>
  );
}

4. 使用 Zod 進行驗證

// actions.ts
'use server';

import { z } from 'zod';
import { revalidatePath } from 'next/cache';

const PostSchema = z.object({
  title: z.string().min(3, 'Tiêu đề ít nhất 3 ký tự').max(100),
  content: z.string().min(10, 'Nội dung ít nhất 10 ký tự'),
});

export async function createPost(prevState: State, formData: FormData) {
  const parsed = PostSchema.safeParse({
    title: formData.get('title'),
    content: formData.get('content'),
  });

  if (!parsed.success) {
    return {
      errors: parsed.error.flatten().fieldErrors,
      message: 'Validation failed',
    };
  }

  try {
    await db.post.create({ data: parsed.data });
    revalidatePath('/posts');
    return { message: 'Tạo thành công!' };
  } catch {
    return { message: 'Có lỗi xảy ra' };
  }
}

5. 樂觀的更新

'use client';
import { useOptimistic } from 'react';
import { toggleLike } from './actions';

export function LikeButton({ liked, count }: { liked: boolean; count: number }) {
  const [optimistic, addOptimistic] = useOptimistic(
    { liked, count },
    (state, newLiked: boolean) => ({
      liked: newLiked,
      count: newLiked ? state.count + 1 : state.count - 1,
    })
  );

  return (
    <form action={async () => {
      addOptimistic(!optimistic.liked);
      await toggleLike();
    }}>
      <button type="submit">
        {optimistic.liked ? '❤️' : '🤍'} {optimistic.count}
      </button>
    </form>
  );
}

6. 刪除和內聯操作

// Inline server action with bind
import { deletePost } from './actions';

export default async function PostList() {
  const posts = await db.post.findMany();

  return (
    <ul>
      {posts.map(post => {
        const deleteWithId = deletePost.bind(null, post.id);
        return (
          <li key={post.id}>
            {post.title}
            <form action={deleteWithId}>
              <button type="submit">Xóa</button>
            </form>
          </li>
        );
      })}
    </ul>
  );
}

7. 比較伺服器操作與 API 路由

特點伺服器操作API 路由
漸進增強✅ 無需 JS 即可運作❌需要JS
類型安全✅ 端到端⚠️手冊
重新驗證✅ 內置⚠️手冊
使用案例UI 的突變外部 API、網路鉤子

下一篇: 使用 NextAuth.js 進行身份驗證 — 登入、授權、會話管理。