اخبار وب

React 19 و Server Functions؛ راهنمای عملی Server Actions

Server Functions و Server Actions در React 19 با مثال کامل Next.js و useActionState؛ اعتبارسنجی سرور، وضعیت ارسال، مدیریت خطا و مرز امنیتی.

1404/10/29 2 دقیقه مطالعه 2,235 بازدید
React 19 و Server Functions؛ راهنمای عملی Server Actions

Server Functions در React راهی برای فراخوانی منطق سرور از رابط برنامه فراهم می‌کنند، اما معرفی آن‌ها به‌عنوان قابلیت تازه «React 20» درست نیست. طبق فهرست رسمی نسخه‌ها، در زمان ویرایش این مقاله، ۱۰ اکتبر ۲۰۲۶، نسخه جاری React 19.3 است. این راهنما درباره APIهای واقعی و مرز میان مرورگر و سرور است؛ ادعاهای درصدی درباره کاهش حجم bundle یا افزایش قطعی سرعت را هم بدون سنجش پروژه به آن نسبت نمی‌دهیم.

برای فهم کاربرد این قابلیت، یک فرم ساده را در نظر بگیرید. کاربر نامی وارد می‌کند، سرور ورودی را بررسی می‌کند و پاسخ به همان رابط برمی‌گردد. در مسیر سنتی ممکن است endpoint را دستی بنویسید و در مرورگر با fetch آن را فراخوانی کنید. در محیطی که از Server Functions پشتیبانی می‌کند، بخشی از این اتصال را فریم‌ورک فراهم می‌کند. درخواست شبکه همچنان وجود دارد؛ فقط قرارداد نوشتن و اتصال آن تغییر می‌کند.

Server Function، Server Action و Server Component

Server Function تابعی است که روی سرور اجرا می‌شود و می‌تواند از سمت کلاینت فراخوانی شود. وقتی آن را در یک Action به کار می‌گیرید، مثلاً برای ارسال فرم، اصطلاح Server Action هم مطرح می‌شود. Server Component مفهوم دیگری است و به محل اجرای کامپوننت و تولید خروجی رابط مربوط می‌شود. دانستن این تفاوت جلوی یک خطای رایج را می‌گیرد: هر کامپوننت سروری یک endpoint قابل فراخوانی نیست و هر Action هم الزاماً تابع سرور نیست.

دستور 'use server' در محیط سازگار، تابع یا export مناسب را به‌عنوان منطق سروری مشخص می‌کند. این دستور در یک پروژه ساده React که فقط با Vite در مرورگر اجرا می‌شود، به‌تنهایی سرور نمی‌سازد. به فریم‌ورک یا زیرساختی نیاز دارید که تبدیل، انتقال درخواست و اجرای سمت سرور را فراهم کند. مستندات رسمی Server Functions همین قابلیت را در محدوده پشتیبانی محیط اجرا توضیح می‌دهند.

چه مسئله‌ای ساده‌تر می‌شود؟

برای بعضی عملیات مرتبط با رابط، تعداد بخش‌هایی که دستی به هم وصل می‌کنید کمتر می‌شود. فرم می‌تواند به Action متصل شود و نتیجه در state آن نمایش داده شود. این موضوع برای یک عملیات کوچک مثل ثبت ترجیح کاربر یا ویرایش یک یادداشت مفید است. اما داشتن Server Function به معنی حذف نیاز به طراحی API برای تمام محصول نیست. API عمومی، مصرف توسط اپ موبایل یا ارتباط چند سرویس همچنان ممکن است قرارداد مستقل نیاز داشته باشد.

همچنین نباید از کمتر شدن کد اتصال، نتیجه عملکرد قطعی بگیرید. زمان پاسخ به اعتبارسنجی، دسترسی داده و سرویس‌های بیرونی وابسته است. اگر Action منتظر یک Query سنگین بماند، قرار دادن آن در فایل سرور، Query را سریع نمی‌کند. انتخاب این قابلیت باید با سادگی جریان کاری و تناسب آن با محیط محصول توجیه شود، نه با یک درصد عمومی که به همه برنامه‌ها نسبت داده شده است.

مثال کامل در Next.js App Router

نمونه زیر یک فرم سلام است. اطلاعاتی در پایگاه داده ذخیره نمی‌کند، حساب کاربری ندارد و به API بیرونی وابسته نیست. هدف نشان دادن مرز اجرا، اعتبارسنجی سمت سرور و نمایش وضعیت در رابط است. برای اجرا، یک پروژه Next.js با TypeScript و App Router بسازید. از نسخه پایدار فعلی و وابستگی‌های هماهنگ تولیدشده توسط ابزار ساخت پروژه استفاده کنید؛ دستور را در پروژه قدیمی با نسخه‌های نامرتبط اجرا نکنید.

npx create-next-app@latest server-action-demo --ts --app --eslint --use-npm
cd server-action-demo

هنگام ساخت، App Router را انتخاب کنید. اگر پوشه src را فعال کرده‌اید، مسیرهای زیر را زیر src/app قرار دهید؛ در ساختار بدون src، همان app درست است. فایل layout.tsx تولیدشده را نگه دارید. سه فایل زیر را ایجاد یا جایگزین کنید. مثال به CSS اختصاصی، تصویر یا کامپوننت معرفی‌نشده نیاز ندارد و همه عناصر فرم در markup آمده‌اند.

فایل app/actions.ts: تابعی که روی سرور اجرا می‌شود

'use server'

export type FormState = { message: string; error: string }

export async function greet(
  _previousState: FormState,
  formData: FormData,
): Promise<FormState> {
  const value = formData.get('name')
  if (typeof value !== 'string') {
    return { message: '', error: 'نام معتبر وارد کنید.' }
  }
  const name = value.trim()
  if (name.length < 1 || name.length > 40) {
    return { message: '', error: 'نام باید بین ۱ تا ۴۰ نویسه باشد.' }
  }
  return { message: `سلام ${name}؛ این پاسخ روی سرور ساخته شده است.`, error: '' }
}

ورودی FormData را قابل اعتماد فرض نکرده‌ایم. مقدار name ممکن است وجود نداشته باشد یا نوع مورد انتظار را نداشته باشد؛ سپس طول نام بعد از trim بررسی می‌شود. این بررسی نمونه‌ای کوچک از اعتبارسنجی است. برای داده پیچیده، Schema روشن لازم دارید؛ مقاله اعتبارسنجی Runtime با Zod تفاوت آن را با بررسی نوع TypeScript توضیح می‌دهد.

فایل app/name-form.tsx: رابط و وضعیت ارسال

'use client'

import { useActionState } from 'react'
import { greet, type FormState } from './actions'

const initialState: FormState = { message: '', error: '' }

export default function NameForm() {
  const [state, formAction, pending] = useActionState(greet, initialState)
  return (
    <form action={formAction} aria-busy={pending}>
      <label htmlFor="name">نام شما</label>
      <input id="name" name="name" required maxLength={40} autoComplete="given-name" />
      <button type="submit" disabled={pending}>
        {pending ? 'در حال ارسال…' : 'دریافت سلام'}
      </button>
      {state.error && <p role="alert">{state.error}</p>}
      <p role="status">{state.message}</p>
    </form>
  )
}

useActionState سه مقدار می‌دهد: وضعیت فعلی، Action متصل به فرم و وضعیت pending. در نمونه، دکمه هنگام ارسال غیرفعال است و پیام خطای مورد انتظار داخل همان فرم نمایش داده می‌شود. نام input باید با کلیدی که سرور از FormData می‌خواند، یکسان باشد. ویژگی required و maxLength به تجربه ورودی کمک می‌کنند، ولی اعتبارسنجی سرور را حذف نمی‌کنند.

فایل app/page.tsx: صفحه‌ای که فرم را نشان می‌دهد

import NameForm from './name-form'

export default function Page() {
  return (
    <main dir="rtl">
      <h1>مثال Server Function</h1>
      <p>نام خود را بنویسید تا پاسخ از سرور دریافت شود.</p>
      <NameForm />
    </main>
  )
}

با npm run dev پروژه را اجرا کنید و نشانی محلی اعلام‌شده در خروجی را باز کنید. نامی وارد کنید و نتیجه را ببینید. متن پاسخ در actions.ts ساخته می‌شود و در کامپوننت فرم نمایش داده می‌شود. این نمونه را با عملیات ذخیره واقعی اشتباه نگیرید: ارسال موفق سلام به معنی ثبت کاربر، مدیریت Session یا انجام احراز هویت نیست. برای افزودن آن کارها باید قرارداد و کنترل‌های مربوط را جدا طراحی کنید.

چرا previousState اولین آرگومان است؟

وقتی تابع را با useActionState به کار می‌برید، وضعیت قبلی به‌عنوان آرگومان اول و داده ارسال‌شده بعد از آن می‌آید. به همین دلیل امضای greet در مثال، دو ورودی دارد. حذف previousState و خواندن آرگومان اول به‌عنوان FormData یک اشتباه متداول است. حتی اگر در این فرم از وضعیت قبلی استفاده نمی‌کنیم، جای آن در قرارداد تابع مهم است؛ نام با پیشوند underscore همین استفاده نشدن را روشن می‌کند.

در مستندات useActionState، اتصال Action و وضعیت pending توضیح داده شده است. اگر Action را بیرون مسیر مناسب فراخوانی کنید، نباید انتظار داشته باشید تمام رفتار Transition خودکار حفظ شود. در فرم این نمونه، اتصال از طریق prop به نام action انجام شده است. برای فراخوانی مستقیم در رویداد دیگری، قرارداد Transition و رفتار خطا را مطابق مستندات همان نسخه رعایت کنید.

مرز امنیتی همچنان سمت سرور است

تابع سرور را مثل یک ورودی قابل دسترس از شبکه در نظر بگیرید. پنهان کردن دکمه در رابط، مجوز اجرای عملیات را تعیین نمی‌کند. اگر تابع قرار است یادداشت یک کاربر را ویرایش کند، باید روی سرور احراز هویت و مالکیت همان یادداشت بررسی شود. داشتن شناسه در FormData یا bind کردن آن به تابع، به‌تنهایی اثبات نمی‌کند درخواست‌کننده اجازه تغییر آن داده را دارد.

هر ورودی، حتی مقداری که ظاهراً از انتخاب محدود رابط می‌آید، باید با قرارداد سرور تطبیق داده شود. TypeScript در زمان توسعه کمک می‌کند، اما یک درخواست دستکاری‌شده را در Runtime معتبر نمی‌کند. همچنین پیام خطا نباید جزئیات محرمانه، ساختار داخلی پایگاه داده یا token را به مرورگر برگرداند. خطای قابل انتظار را با پیام مناسب کاربر مدیریت کنید و اطلاعات فنی لازم را در مسیر گزارش تیم نگه دارید.

عملیات تکراری، خطا و وضعیت خوش‌بینانه

غیرفعال کردن دکمه در حالت pending از بعضی ارسال‌های ناخواسته جلوگیری می‌کند، ولی تضمین یکتایی عملیات نیست. کاربر ممکن است صفحه دیگری باز کند یا درخواست قبلی در شرایط شبکه نامشخص بماند. برای عملی مثل پرداخت یا ثبت سفارش، کنترل تکرار باید در سرور و لایه داده طراحی شود. UI خوب به کاربر کمک می‌کند؛ مسئولیت درست بودن نتیجه تجاری را بر عهده نمی‌گیرد.

نمایش Optimistic UI زمانی مفید است که نتیجه احتمالی را زودتر نشان دهید و در صورت خطا بتوانید آن را اصلاح کنید. این راهکار برای همه عملیات مناسب نیست. اضافه شدن موقت یک مورد به فهرست با اعلام موفقیت قطعی پرداخت فرق دارد. اگر وضعیت خوش‌بینانه به کار می‌برید، مسیر برگشت، پیام خطا و هماهنگی داده تازه را هم طراحی کنید. در غیر این صورت سرعت ظاهری رابط با ابهام نتیجه همراه می‌شود.

پس از تغییر داده، صفحه چطور تازه می‌شود؟

Server Function به‌تنهایی سیاست Cache تمام برنامه را تعیین نمی‌کند. وقتی داده‌ای تغییر می‌کند، باید مشخص کنید کدام صفحه یا منبع دوباره خوانده شود. فریم‌ورک‌ها ابزارهای مختلفی برای این کار دارند و نام و رفتار آن‌ها به نسخه و محیط مربوط است. یک فراخوانی موفق را مدرک تازه شدن خودکار همه فهرست‌ها ندانید. اگر صفحه نتیجه قدیمی نشان می‌دهد، مسیر خواندن داده و Cache را جدا از مسیر نوشتن بررسی کنید.

برای تیم، بهتر است قرارداد هر عملیات کوتاه و روشن باشد: چه ورودی می‌گیرد، چه مجوزی می‌خواهد، چه چیزی تغییر می‌دهد و چه نتیجه‌ای برمی‌گرداند. این قرارداد به انتخاب میان Server Function، endpoint مستقل یا ابزار داده کمک می‌کند. اگر برنامه API عمومی برای مصرف‌کننده دیگر دارد، طراحی آن را فقط برای هماهنگی با یک فرم داخلی کنار نگذارید.

مهاجرت از پروژه قبلی

اول نسخه React، فریم‌ورک و پشتیبانی واقعی RSC را بررسی کنید. تغییر وابستگی به نسخه جدید، یک برنامه کاملاً client-side را خودکار به برنامه دارای Server Functions تبدیل نمی‌کند. برای پروژه موجود، یک عملیات کم‌خطر و مشخص انتخاب کنید و قرارداد قبلی را با مسیر جدید مقایسه کنید. احراز هویت، پیام خطا و سیاست تازه شدن داده نباید در جریان کوتاه کردن کد گم شوند.

Server Functions برای نزدیک کردن منطق مرتبط با رابط به مسیر سرور ابزار مفیدی هستند. استفاده درست آن‌ها از شناخت مرز اجرا و ورودی شروع می‌شود. در مثال این مقاله، فرم کامل، اعتبارسنجی سرور و وضعیت ارسال کنار هم آمده‌اند تا خواننده برای اجرای نمونه مجبور به حدس زدن ساختار نباشد. برای محصول واقعی، همین وضوح را به قرارداد داده، مجوزها و نتیجه عملیات گسترش دهید.

واکنش به این مقاله

اشتراک‌گذاری این مقاله:
دیدگاه‌ها 0
۰ / ۲۰۰۰

هنوز دیدگاهی ثبت نشده

اولین نفری باشید که نظر می‌دهد!