Server Actions ב-Next.js 16: המדריך המלא לטפסים, מוטציות ואבטחה

מדריך מקיף ל-Server Actions ב-Next.js 16 ו-React 19: איך לבנות טפסים מודרניים, לבצע מוטציות בטוחות, לשלב useActionState ו-useOptimistic ולשלב עם Cache Components — כולל דוגמאות קוד מעשיות והגנה מפני CSRF.

Next.js 16 Server Actions: מדריך מלא 2026

אם הגעת לכאן, סביר להניח שאתה מתחבט בשאלה איך לבנות טפסים, מוטציות נתונים ופעולות צד-שרת ב-Next.js 16 בלי לכתוב Route Handlers ידניים, בלי להתעסק עם fetch מצד הלקוח, ובלי לכאוב את הראש סביב CSRF. אז כן — כאן בדיוק נכנסות Server Actions. זה המנגנון שהפך בשקט-בשקט לדרך הסטנדרטית לבצע פעולות צד-שרת ב-App Router, ובמדריך הזה נעבור על הכול: מ-Hello World ועד דפוסי פרודקשן רציניים שעובדים יפה ב-Next.js 16 עם React 19.

קצת רקע לפני שצוללים. במהלך 2026, אחרי שחיכינו (יותר מדי, אם תשאלו אותי) לייצוב, Server Actions סוף-סוף עברו את הסף ונחשבים יציבים לחלוטין בקוד פרודקשן. אם נתקלת באיזשהו מדריך ישן שעדיין מדבר על experimental.serverActions — אפשר לשכוח. הדגל הזה הוסר עוד ב-Next.js 14, ובגרסה 16 ה-API נקי, ממוטב ועובד היטב יחד עם Cache Components ועם proxy.ts החדש.

מה זה בעצם Server Action?

Server Action היא פונקציה אסינכרונית שרצה אך ורק על השרת, אבל אפשר לקרוא לה ישירות מקומפוננטת לקוח כאילו הייתה פונקציה רגילה. מאחורי הקלעים, Next.js מייצרת endpoint ייעודי, מטפלת בסריאליזציה של הארגומנטים, מוסיפה הגנת CSRF מובנית ומחזירה את הערך בצורה שקופה. במילים אחרות — הרבה boilerplate שכבר לא צריך לכתוב.

המפתח הוא ההנחיה "use server". היא יכולה להופיע בשני מקומות:

  • בראש קובץ — כל ייצוא בקובץ הופך ל-Server Action.
  • בתחילת גוף פונקציה בתוך Server Component — הופך רק אותה פונקציה ל-Action.

דוגמה מינימלית

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

import { db } from "@/lib/db";
import { revalidatePath } from "next/cache";

export async function createNote(formData: FormData) {
  const title = formData.get("title")?.toString() ?? "";
  if (!title.trim()) return { error: "כותרת חובה" };

  await db.note.create({ data: { title } });
  revalidatePath("/notes");
  return { ok: true };
}

וכך משתמשים בזה ישירות מטופס בקומפוננטת שרת:

// app/notes/page.tsx
import { createNote } from "@/app/actions/notes";

export default function NotesPage() {
  return (
    <form action={createNote}>
      <input name="title" required />
      <button type="submit">צור פתק</button>
    </form>
  );
}

שים לב — אין onSubmit, אין fetch, אין endpoint נפרד. והכי יפה: הטופס עובד גם בלי JavaScript בדפדפן (Progressive Enhancement, מובנה לגמרי).

השוואה מהירה: Server Actions מול Route Handlers

זאת אולי השאלה הכי נפוצה שאני שומע. מתי לבחור במה? ב-Next.js 16 ההבחנה התחדדה ממש:

  • Server Actions — לכל מוטציה פנימית מתוך האפליקציה: יצירה, עדכון, מחיקה, שליחת טופס, פעולת מנהל וכו'.
  • Route Handlers (app/api/.../route.ts) — ל-API ציבורי, webhooks חיצוניים (Stripe, Clerk וחבריהם), endpoints שצורכים אותם clients שאינם הדפדפן שלך (mobile, צד-שלישי).

חוק אצבע פשוט: אם רק האפליקציה שלך קוראת לפונקציה — Server Action. אם מישהו חיצוני (curl, Postman, webhook) קורא לה — Route Handler. ככה זה.

טפסים מודרניים עם useActionState

React 19 הביא איתו hook חדש בשם useActionState, שמחליף את useFormState הישן. הוא מנהל עבורך את ה-state של הפעולה, מאפשר ולידציה הדרגתית, ובונוס — מחזיר גם דגל pending.

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

import { z } from "zod";
import { redirect } from "next/navigation";

const Schema = z.object({
  email: z.string().email("אימייל לא תקין"),
  password: z.string().min(8, "סיסמה חייבת לפחות 8 תווים"),
});

export type SignupState = {
  errors?: Record<string, string[]>;
  message?: string;
};

export async function signup(
  _prev: SignupState,
  formData: FormData
): Promise<SignupState> {
  const parsed = Schema.safeParse({
    email: formData.get("email"),
    password: formData.get("password"),
  });

  if (!parsed.success) {
    return { errors: parsed.error.flatten().fieldErrors };
  }

  // ... יצירת משתמש בפועל
  redirect("/dashboard");
}
// app/signup/form.tsx
"use client";

import { useActionState } from "react";
import { useFormStatus } from "react-dom";
import { signup, type SignupState } from "@/app/actions/signup";

const initialState: SignupState = {};

function SubmitButton() {
  const { pending } = useFormStatus();
  return (
    <button type="submit" disabled={pending}>
      {pending ? "רושם..." : "הרשם"}
    </button>
  );
}

export function SignupForm() {
  const [state, formAction] = useActionState(signup, initialState);

  return (
    <form action={formAction}>
      <input name="email" type="email" />
      {state.errors?.email && <p>{state.errors.email[0]}</p>}

      <input name="password" type="password" />
      {state.errors?.password && <p>{state.errors.password[0]}</p>}

      <SubmitButton />
    </form>
  );
}

השילוב של useActionState עם useFormStatus נותן חוויה ממש עשירה: שגיאות מוצגות לכל שדה בנפרד, הכפתור ננעל בזמן השליחה, וה-redirect מתבצע אטומית בצד השרת. פעם הייתי מטפל בכל זה ידנית עם useState ו-fetch, וכל פרויקט נראה אחרת — היום פשוט אין סיבה.

Optimistic Updates עם useOptimistic

אחת התכונות האהובות עליי ב-React 19 שמשתלבת מעולה ב-Server Actions היא useOptimistic. במקום לחכות לתגובת השרת, ה-UI מתעדכן מיידית — ואם הפעולה נכשלת, הוא חוזר אחורה לבד. קסם.

"use client";

import { useOptimistic } from "react";
import { addLike } from "@/app/actions/posts";

export function LikeButton({ post }: { post: { id: string; likes: number } }) {
  const [optimisticLikes, addOptimisticLike] = useOptimistic(
    post.likes,
    (current) => current + 1
  );

  async function handleClick() {
    addOptimisticLike(1);
    await addLike(post.id);
  }

  return (
    <button onClick={handleClick}>
      ❤ {optimisticLikes}
    </button>
  );
}

אבטחה: מה Next.js עושה בשבילך — ומה לא

אחת הטעויות הכי נפוצות שאני רואה אצל מפתחים (וגם אצלי בעבר, בלי בושה) היא הנחה ש-Server Actions "מוגנות אוטומטית". זה נכון רק חלקית. הנה מה שקורה בפועל:

מה Next.js עושה לבד

  • הגנת CSRF — Next.js בודקת אוטומטית את כותרת Origin מול Host ומסרבת לבקשות cross-origin.
  • Action IDs לא צפויים — לכל Action מוקצה מזהה מבוסס hash שלא ניתן לנחש; לא מייצאים ידנית את ה-endpoint.
  • Dead Code Elimination — Action שלא מיובאת בשום מקום מקומפוננטה תיחתך מה-build ולא תהיה נגישה.

מה אתה חייב לעשות בעצמך

  1. אימות זהות (Authentication) — תמיד תבדוק session בתוך ה-Action, לא רק במסך שמציג את הטופס. שכבת הטופס לא שווה כלום אם מישהו קורא ל-Action ישירות.
  2. הרשאות (Authorization) — האם המשתמש רשאי לבצע את הפעולה הספציפית הזאת על המשאב הספציפי הזה?
  3. ולידציה — אף פעם, באמת אף פעם, לא לסמוך על FormData בלי לעבור דרך סכמה (Zod / Valibot).
  4. Rate Limiting — Server Actions הן endpoints לכל דבר; הוסף הגבלה ב-proxy.ts או באמצעות Upstash Ratelimit בתוך ה-Action.

תבנית עטיפה (wrapper) להגנה אחידה

אני אישית תמיד עוטף Actions שדורשות התחברות ב-helper יחיד. זה מונע ממך לשכוח את ה-auth check בלחץ של דדליין:

// lib/safe-action.ts
import { auth } from "@/lib/auth";

export function authedAction<T>(
  fn: (userId: string, formData: FormData) => Promise<T>
) {
  return async (formData: FormData) => {
    const session = await auth();
    if (!session?.user?.id) {
      throw new Error("UNAUTHORIZED");
    }
    return fn(session.user.id, formData);
  };
}
// app/actions/posts.ts
"use server";

import { authedAction } from "@/lib/safe-action";
import { db } from "@/lib/db";

export const deletePost = authedAction(async (userId, formData) => {
  const id = formData.get("id")?.toString();
  if (!id) throw new Error("חסר מזהה");

  const post = await db.post.findUnique({ where: { id } });
  if (post?.authorId !== userId) throw new Error("FORBIDDEN");

  await db.post.delete({ where: { id } });
});

שילוב עם Cache Components של Next.js 16

Next.js 16 הציגה את Cache Components ("use cache") — וזה ממש שינה את האופן שבו Server Actions משתלבות עם Cache. ההמלצה שלי, בקצרה:

  • קריאה — קומפוננטות שמציגות נתונים מסומנות עם "use cache" ו-cacheTag('posts').
  • כתיבה — Server Action שמשנה את הנתונים קוראת ל-revalidateTag('posts').
// app/actions/posts.ts
"use server";

import { revalidateTag } from "next/cache";

export async function createPost(formData: FormData) {
  await db.post.create({ data: { title: formData.get("title") as string } });
  revalidateTag("posts");
}
// app/posts/list.tsx
import { cacheTag } from "next/cache";

async function getPosts() {
  "use cache";
  cacheTag("posts");
  return db.post.findMany();
}

השילוב הזה נותן לך התנהגות שהייתה דורשת בעבר Redis ו-SWR — ועכשיו מוכלת באופן מובנה ב-framework. אם זה לא קסם, אני לא יודע מה כן.

קריאה ל-Server Actions ללא טופס

אפשר לקרוא ל-Server Action גם כפונקציה רגילה: מתוך handler, מתוך useEffect (לא מומלץ, רק תיאורטית אפשרי), או — הכי מקובל — מתוך useTransition. הדפוס הנפוץ נראה ככה:

"use client";

import { useTransition } from "react";
import { archivePost } from "@/app/actions/posts";

export function ArchiveButton({ id }: { id: string }) {
  const [isPending, startTransition] = useTransition();

  return (
    <button
      disabled={isPending}
      onClick={() => startTransition(() => archivePost(id))}
    >
      {isPending ? "מארכב..." : "ארכב"}
    </button>
  );
}

useTransition משאיר את ה-UI רספונסיבי בזמן שהפעולה רצה, מבלי לחסום אינטראקציות אחרות. בקיצור — חוויית משתמש טובה יותר ב-3 שורות קוד.

טיפול בשגיאות

Server Actions זורקות חריגות שמתרגמות אוטומטית ל-error.tsx הקרוב ביותר ב-segment. עם זאת, לרוב עדיף להחזיר אובייקט שגיאה מובנה במקום לזרוק. למה? כי ככה אתה יכול להציג שגיאה בטופס בלי לאבד את ה-state שהמשתמש כבר הקליד.

type Result<T> = { ok: true; data: T } | { ok: false; error: string };

export async function updateProfile(formData: FormData): Promise<Result<void>> {
  try {
    // ... לוגיקה
    return { ok: true, data: undefined };
  } catch (err) {
    console.error(err);
    return { ok: false, error: "עדכון נכשל, נסה שוב" };
  }
}

טעויות נפוצות שכדאי להימנע מהן

  1. שימוש ב-Server Action ל-GET — Server Actions תמיד POST. לקריאת נתונים השתמש בקומפוננטות שרת או ב-fetch ישיר.
  2. שכחה של revalidatePath או revalidateTag — הנתונים בצד הלקוח פשוט לא יתעדכנו לבד אחרי מוטציה. (זאת אולי הטעות שהכי גוזלת לי שעות דיבאג של אנשים אחרים.)
  3. החזרת ערכים לא-סריאליזביליים — Date, Map, Set מותרים, אבל Function או Symbol יזרקו שגיאה.
  4. הסתמכות על "use server" בלי אבטחה — כאמור, ההנחיה לא מחליפה auth checks.
  5. קריאה ל-Server Action מ-useEffect — זה anti-pattern; אם אתה צריך לטעון נתונים, עשה את זה ב-Server Component.

שאלות נפוצות (FAQ)

האם Server Actions מחליפות לחלוטין את Route Handlers?

לא, וזה חשוב. Server Actions מצוינות למוטציות פנימיות מתוך ה-UI שלך, אבל ל-webhooks חיצוניים (Stripe, GitHub), API ציבורי לצרכנים מובייליים או צד-שלישי, וכל endpoint שצריך GET — Route Handlers עדיין הכלי הנכון.

האם Server Actions עובדות עם React 18 או רק עם React 19?

Next.js 16 מצריך React 19. ב-React 19 קיבלת את useActionState, useOptimistic ושיפורי useFormStatus שהופכים את החוויה למלאה. אם אתה תקוע ב-React 18 עם Next.js 14, השתמש ב-useFormState הישן עד שתעלה גרסה.

איך מאבטחים Server Action מפני התקפות CSRF?

Next.js מבצעת אוטומטית בדיקת Origin-vs-Host וחוסמת בקשות שלא מגיעות מאותו origin. בנוסף, מזהי הפעולות (Action IDs) מבוססי hash לא ניתנים לניחוש. עם זאת — ואני לא יכול להדגיש את זה מספיק — אתה עדיין חייב לבצע אימות זהות והרשאות בתוך ה-Action עצמה.

איך בודקים (testing) Server Actions?

מאחר ש-Server Actions הן פונקציות אסינכרוניות רגילות שמייצאות מקובץ, אפשר לבדוק אותן ב-Vitest כמו כל פונקציה: לייבא את ה-Action, להעביר FormData מזויף, ולוודא שהיא קוראת ל-DB כצפוי. לבדיקות end-to-end? Playwright שמטריגר את הטופס בדפדפן אמיתי.

מה ההבדל בין revalidatePath ל-revalidateTag?

revalidatePath מבטל את ה-cache של מסלול ספציפי (URL). revalidateTag מבטל את כל פיסות ה-cache שתויגו בתג מסוים — דפוס חזק יותר, במיוחד כשאותם נתונים מוצגים בעמודים שונים. ב-Next.js 16 עם Cache Components, הטכניקה המומלצת היא לתייג קומפוננטות עם cacheTag ולקרוא ל-revalidateTag מתוך ה-Action.

סיכום

Server Actions ב-Next.js 16 הם הרבה יותר מ-"דרך נחמדה להריץ קוד שרת מטופס". הם המנגנון שמאחד מוטציות, אבטחה, cache invalidation ו-Progressive Enhancement תחת API אחד נקי. אם תאמץ את הדפוסים שעברנו עליהם — wrappers עם auth מובנה, ולידציה עם Zod, useActionState + useOptimistic לחוויית משתמש חלקה, ושילוב עם Cache Components — תקבל אפליקציה שתפעל מהר, תהיה בטוחה, וגם יחסית פשוטה לתחזוקה.

העצה האחרונה שלי: אל תנסה להמיר את כל הפרויקט בבת אחת. תתחיל מטופס אחד פשוט, תרגיש את הזרימה, ותרחיב משם החוצה. בהצלחה.

אודות הכותב Editorial Team

Our team of expert writers and editors.