اکشن های سرور و جهش ها (mutations موتاسیون)
اکشنهای سرور، توابع ناهمزمان (asynchronous functions) هستند که در سمت سرور اجرا میشوند. آنها را میتوان در کامپوننتهای سرور و کلاینت برای مدیریت ارسال فرم و تغییر (موتاسیون) داده در اپلیکیشنهای Next.js استفاده کرد.
🎥 تماشا کنید: در مورد فرم ها و جهش ها با اکشن های سرور بیشتر بیاموزید → YouTube (10 دقیقه) (opens in a new tab).
قرارداد
یک اکشن سرور را میتوان با استفاده از دستورالعمل "use server" (opens in a new tab) در React تعریف کرد. شما میتوانید این دستور را در ابتدای یک تابع ناهمزمان async قرار دهید تا آن را به عنوان یک اکشن سرور مشخص کنید، یا در ابتدای یک فایل جداگانه قرار دهید تا همه خروجیهای آن فایل به عنوان اکشنهای سرور در نظر گرفته شوند.
کامپوننتهای سرور
کامپوننتهای سرور میتوانند از دستورالعمل سطح تابع یا سطح ماژول "use server" بصورت درون خطی (inline) استفاده کنند. برای درون خطی کردن یک اکشن سرور، "use server" را به ابتدای بدنه تابع اضافه کنید:
// Server Component
export default function Page() {
// Server Action
async function create() {
'use server'
// ...
}
return (
// ...
)
}کامپوننت های کلاینت
کامپوننتهای کلاینت تنها میتوانند اکشنهایی را ایمپورت کنند که از دستورالعمل سطح ماژول "use server" استفاده میکنند.
برای فراخوانی یک اکشن سرور در یک کامپوننت کلاینت، یک فایل جدید ایجاد کرده و دستورالعمل "use server" را در ابتدای آن اضافه کنید. همه توابع درون فایل به عنوان اکشنهای سرور در نظر گرفته میشوند که میتوانند در هر دو کامپوننت سرور و کلاینت استفاده مجدد شوند:
'use server'
export async function create() {
// ...
}import { create } from '@/app/actions'
export function Button() {
return (
// ...
)
}همچنین میتوانید یک اکشن سرور را به عنوان prop به یک کامپوننت کلاینت پاس دهید:
<ClientComponent updateItem={updateItem} />'use client'
export default function ClientComponent({ updateItem }) {
return <form action={updateItem}>{/* ... */}</form>
}رفتار
- اکشنهای سرور را میتوان با استفاده از صفت action در تگ
<form>فراخوانی کرد:- کامپوننتهای سرور به طور پیش فرض از پیشرفت تدریجی (progressive enhancement) پشتیبانی میکنند، به این معنی که حتی اگر جاوااسکریپت بارگیری نشده باشد یا غیرفعال شود، فرم همچنان ارسال میشود.
- در کامپوننتهای کلاینت، فرمهایی که اکشنهای سرور را فراخوانی میکنند، در صورتی که جاوااسکریپت هنوز بارگیری نشده باشد، ارسالها را در صف قرار میدهند و اولویت را به hydration کلاینت میدهند.
- بعد از hydration، مرورگر هنگام ارسال فرم، صفحه را دوباره بارگذاری نمیکند.
- اکشنهای سرور محدود به
<form>نیستند و میتوانند از event handlerها،useEffect، کتابخانههای شخص ثالث و سایر عناصر فرم مانند<button>فراخوانی شوند. - اکشنهای سرور با معماری کش و اعتبارسنجی مجدد (revalidation) Next.js ادغام میشوند. هنگامی که یک اکشن فراخوانی میشود، Next.js میتواند هم رابط کاربری بهروزرسانیشده و هم دادههای جدید را در یک دور رفت و برگشت سرور بازگرداند.
- در پشت صحنه، اکشنها از متد
POSTاستفاده میکنند و فقط این متد HTTP میتواند آنها را فراخوانی invoke کند. - آرگومانها و مقدار برگشتی اکشنهای سرور باید توسط React قابل سریالسازی باشند. برای لیستی از آرگومانها و مقادیر قابل سریالسازی (opens in a new tab)، به مستندات React مراجعه کنید.
- اکشنهای سرور تابع هستند. این بدان معناست که میتوان آنها را در هر جای برنامه خود دوباره استفاده کرد.
- اکشنهای سرور runtime را از صفحهای که در آن استفاده میشوند یا طرح بندی به ارث میبرند.
- اکشنهای سرور پیکربندی بخش مسیر Route Segment Config را از صفحه یا طرح بندی که در آن استفاده میشوند به ارث میبرند، از جمله فیلدهایی مانند
maxDuration.
مثال ها
فرم ها
React المنت HTML <form> (opens in a new tab) را برای اجازه دادن به فراخوانی اکشنهای سرور با پراپ action گسترش میدهد.
هنگامی که در یک فرم فراخوانی میشود، action به طور خودکار شیء FormData (opens in a new tab) را دریافت میکند. شما نیازی به استفاده از React useState برای مدیریت فیلدها ندارید، در عوض میتوانید با استفاده از متدهای FormData (opens in a new tab) بومی، دادهها را استخراج کنید :
export default function Page() {
async function createInvoice(formData: FormData) {
'use server'
const rawFormData = {
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
}
// mutate data
// revalidate cache
}
return <form action={createInvoice}>...</form>
}خوب است بدانید :
- مثال: فرم با حالت بارگیری و خطا (opens in a new tab)
- هنگام کار با فرمهایی که فیلدهای زیادی دارند، ممکن است بخواهید از متد
entries()(opens in a new tab) باObject.fromEntries()(opens in a new tab) جاوااسکریپت استفاده کنید. به عنوان مثال:const rawFormData = Object.fromEntries(formData). یک نکته قابل توجه این است کهformDataشامل ویژگیهای اضافی$ACTION_خواهد بود.- برای اطلاعات بیشتر به مستندات React
<form>(opens in a new tab) مراجعه کنید.
ارسال آرگومانهای اضافی
میتوانید با استفاده از متد bind جاوااسکریپت، آرگومانهای اضافی را به یک اکشن سرور ارسال کنید.
'use client'
import { updateUser } from './actions'
export function UserProfile({ userId }: { userId: string }) {
const updateUserWithId = updateUser.bind(null, userId)
return (
<form action={updateUserWithId}>
<input type="text" name="name" />
<button type="submit">Update User Name</button>
</form>
)
}اکشن سرور علاوه بر دادههای فرم، آرگومان userId را دریافت خواهد کرد:
'use server'
export async function updateUser(userId, formData) {
// ...
}خوب است بدانید:
- روش جایگزین این است که آرگومانها را به عنوان فیلدهای ورودی مخفی در فرم ارسال کنید (برای مثال
<input type="hidden" name="userId" value={userId} />). با این حال، مقدار بخشی از HTML رندر شده خواهد بود و کدگذاری نمیشود..bindهم در کامپوننتهای سرور و هم در کامپوننتهای کلاینت کار میکند. همچنین از پیشرفت تدریجی progressive enhancement پشتیبانی میکند.
حالتهای در حال انتظار
میتوانید از هوک useFormStatus (opens in a new tab) React برای نمایش حالت در حال انتظار در هنگام ارسال فرم استفاده کنید.
useFormStatusوضعیت یک فرم خاص را برمیگرداند، بنابراین باید به عنوان فرزند المنت<form>تعریف شود.useFormStatusیک قلاب React است و بنابراین باید در یک کامپوننت کلاینت استفاده شود.
'use client'
import { useFormStatus } from 'react-dom'
export function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending}>
Add
</button>
)
}سپس <SubmitButton /> را میتوان در هر فرمی تو در تو قرار داد:
import { SubmitButton } from '@/app/submit-button'
import { createItem } from '@/app/actions'
// Server Component
export default async function Home() {
return (
<form action={createItem}>
<input type="text" name="field-name" />
<SubmitButton />
</form>
)
}اعتبارسنجی سمت سرور و هندلینگ خطا
ما توصیه میکنیم برای اعتبارسنجی اولیه فرم در سمت کلاینت از اعتبارسنجی HTML مانند required و type="email" استفاده کنید.
برای اعتبارسنجی پیشرفتهتر در سمت سرور، میتوانید از کتابخانهای مانند zod (opens in a new tab) برای اعتبارسنجی فیلدهای فرم قبل از تغییر دادهها استفاده کنید:
'use server'
import { z } from 'zod'
const schema = z.object({
email: z.string({
invalid_type_error: 'Invalid Email',
}),
})
export default async function createUser(formData: FormData) {
const validatedFields = schema.safeParse({
email: formData.get('email'),
})
// Return early if the form data is invalid
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}
// Mutate data
}پس از اینکه فیلدها در سرور اعتبارسنجی شدند، میتوانید یک شیء قابل سریالسازی را در اکشن خود برگردانید و از قلاب React useActionState (opens in a new tab) برای نمایش پیامی به کاربر استفاده کنید.
- با پاس دادن اکشن به
useActionState، امضای تابع اکشن تغییر میکند تا پارامتر جدیدی به عنوان اولین آرگومان خود به نامprevStateیاinitialStateدریافت کند. useActionStateیک قلاب React است و بنابراین باید در یک کامپوننت کلاینت استفاده شود.
'use server'
export async function createUser(prevState: any, formData: FormData) {
// ...
return {
message: 'Please enter a valid email',
}
}سپس میتوانید اکشن خود را به قلاب useActionState پاس دهید و از state برگشتی برای نمایش پیام خطا استفاده کنید.
'use client'
import { useActionState } from 'react'
import { createUser } from '@/app/actions'
const initialState = {
message: '',
}
export function Signup() {
const [state, formAction] = useActionState(createUser, initialState)
return (
<form action={formAction}>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" required />
{/* ... */}
<p aria-live="polite" className="sr-only">
{state?.message}
</p>
<button>Sign up</button>
</form>
)
}خوب است بدانید:
- قبل از تغییر دادهها، همیشه باید مطمئن شوید که کاربر مجاز به انجام این کار است. بخش احراز هویت و مجوزدهی را ببینید.
بهروزرسانیهای خوش بینانه
میتوانید از هوک useOptimistic (opens in a new tab) React برای بهروزرسانی خوشبینانه رابط کاربری قبل از اینکه اکشن سرور به پایان برسد، به جای انتظار برای پاسخ، استفاده کنید:
'use client'
import { useOptimistic } from 'react'
import { send } from './actions'
type Message = {
message: string
}
export function Thread({ messages }: { messages: Message[] }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic<
Message[],
string
>(messages, (state, newMessage) => [...state, { message: newMessage }])
return (
<div>
{optimisticMessages.map((m, k) => (
<div key={k}>{m.message}</div>
))}
<form
action={async (formData: FormData) => {
const message = formData.get('message')
addOptimisticMessage(message)
await send(message)
}}
>
<input type="text" name="message" />
<button type="submit">Send</button>
</form>
</div>
)
}عناصر تو در تو
شما میتوانید اکشنهای سرور را در عناصری که درون تگ <form> قرار دارند مانند <button>, <input type="submit"> و <input type="image"> فراخوانی کنید. این عناصر از ویژگی formAction یا رویدادگیرنده event handlers پشتیبانی میکنند.
این قابلیت زمانی مفید است که بخواهید چندین اکشن سرور را درون یک فرم صدا بزنید. برای مثال، میتوانید یک <button> خاص برای ذخیرهی پیشنویس (draft) یک پست به همراه دکمهی انتشار آن ایجاد کنید. برای اطلاعات بیشتر به مستندات تگ <form> در React (opens in a new tab) مراجعه کنید.
ارسال فرم بصورت برنامهای
شما میتوانید با استفاده از متد requestSubmit() (opens in a new tab) ارسال یک فرم را آغاز کنید. برای مثال، زمانی که کاربر کلیدهای ⌘ و Enter را با هم فشار میدهد، میتوانید رویداد onKeyDown را دریافت کنید:
'use client'
export function Entry() {
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (
(e.ctrlKey || e.metaKey) &&
(e.key === 'Enter' || e.key === 'NumpadEnter')
) {
e.preventDefault()
e.currentTarget.form?.requestSubmit()
}
}
return (
<div>
<textarea
name="entry"
rows={20}
required
onKeyDown={handleKeyDown}
/>
</div>
)
}این کار باعث ارسال نزدیکترین <form> پدر (nearest form ancestor) میشود، که در نتیجه اکشن سرور فراخوانی خواهد شد.
عناصر غیرفرم Non-form Elements
در حالی که استفاده از اکشنهای سرور درون تگهای <form> رایج است، آنها را میتوان از بخشهای دیگر کد شما مانند رویدادگیرندهها (event handlers) و useEffect نیز فراخوانی کرد.
Event Handlers
شما میتوانید یک اکشن سرور را از رویدادگیرندههایی مانند onClick فراخوانی کنید. برای مثال، برای افزایش شمارندهی لایک:
'use client'
import { incrementLike } from './actions'
import { useState } from 'react'
export default function LikeButton({ initialLikes }: { initialLikes: number }) {
const [likes, setLikes] = useState(initialLikes)
return (
<>
<p>Total Likes: {likes}</p>
<button
onClick={async () => {
const updatedLikes = await incrementLike()
setLikes(updatedLikes)
}}
>
Like
</button>
</>
)
}برای بهبود تجربهی کاربری، پیشنهاد میکنیم از APIهای دیگر React مانند useOptimistic (opens in a new tab) و useTransition (opens in a new tab) برای بهروزرسانی رابط کاربری قبل از اینکه اکشن سرور در سمت سرور به پایان برسد، یا برای نمایش حالت در حال انتظار استفاده کنید.
همچنین میتوانید رویدادگیرندهها را به عناصر فرم اضافه کنید، برای مثال برای ذخیرهی فیلد فرم با رویداد onChange:
'use client'
import { publishPost, saveDraft } from './actions'
export default function EditPost() {
return (
<form action={publishPost}>
<textarea
name="content"
onChange={async (e) => {
await saveDraft(e.target.value)
}}
/>
<button type="submit">Publish</button>
</form>
)
}در مواردی مانند این که چندین رویداد ممکن است به سرعت پشت سر هم فعال شوند، استفاده از تکنیک debouncing برای جلوگیری از فراخوانیهای غیرضروری اکشن سرور را پیشنهاد میکنیم.
useEffect
شما میتوانید از قلاب useEffect (opens in a new tab) در React برای فراخوانی یک اکشن سرور زمانی که کامپوننت بارگذاری میشود (mount) یا وابستگی آن تغییر میکند، استفاده کنید. این کار برای تغییراتی (موتاسیونهایی) که به رویدادهای سراسری وابستهاند یا نیاز دارند به صورت خودکار راهاندازی شوند، مفید است. برای مثال، onKeyDown برای میانبرهای برنامه (app shortcuts)، یک قلاب intersection observer برای اسکرول بینهایت، یا زمانی که کامپوننت برای بهروزرسانی شمارندهی بازدید بارگذاری میشود.
'use client'
import { incrementViews } from './actions'
import { useState, useEffect } from 'react'
export default function ViewCount({ initialViews }: { initialViews: number }) {
const [views, setViews] = useState(initialViews)
useEffect(() => {
const updateViews = async () => {
const updatedViews = await incrementViews()
setViews(updatedViews)
}
updateViews()
}, [])
return <p>Total Views: {views}</p>
}به خاطر داشته باشید که رفتار (behavior) و نکات مهم (caveats) (opens in a new tab) مربوط به useEffect را در نظر بگیرید.
هندل کردن خطا
زمانی که خطایی پرتاب میشود (thrown)، توسط نزدیکترین مرز error.js یا <Suspense> در کلاینت گرفته میشود. پیشنهاد میکنیم از try/catch برای بازگرداندن خطاها به منظور مدیریت شدن توسط رابط کاربریتان استفاده کنید.
برای مثال، اکشن سرور شما ممکن است خطاهای ناشی از ایجاد یک آیتم جدید را با بازگرداندن یک پیام مدیریت کند :
'use server'
export async function createTodo(prevState: any, formData: FormData) {
try {
// Mutate data
} catch (e) {
throw new Error('Failed to create task')
}
}خوب است بدانید:
- علاوه بر پرتاب کردن خطا، همچنین میتوانید یک شیء را برای مدیریت شدن توسط
useActionStateبازگردانید. بخش اعتبارسنجی و مدیریت خطا در سمت سرور را ببینید.
اعتبارسنجی مجدد داده
شما میتوانید کش Next.js را درون اکشنهای سرور خود با استفاده از API revalidatePath اعتبارسنجی مجدد کنید:
'use server'
import { revalidatePath } from 'next/cache'
export async function createPost() {
try {
// ...
} catch (error) {
// ...
}
revalidatePath('/posts')
}یا یک فراخوانی خاص برای دریافت داده با استفاده از یک تگ کش (cache tag) با revalidateTag نامعتبر کنید:
'use server'
import { revalidateTag } from 'next/cache'
export async function createPost() {
try {
// ...
} catch (error) {
// ...
}
revalidateTag('posts')
}تغییر مسیر Redirecting
اگر میخواهید کاربر را بعد از تکمیل یک اکشن سرور به مسیر (route) دیگری هدایت کنید، میتوانید از API redirect استفاده کنید. redirect نیاز دارد که خارج از بلوک try/catch فراخوانی شود :
'use server'
import { redirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'
export async function createPost(id: string) {
try {
// ...
} catch (error) {
// ...
}
revalidateTag('posts') // Update cached posts
redirect(`/post/${id}`) // Navigate to the new post page
}کوکی ها Cookies
شما میتوانید درون یک اکشن سرور با استفاده از API cookies کوکیها را get، set و delete کنید:
'use server'
import { cookies } from 'next/headers'
export async function exampleAction() {
// Get cookie
const value = cookies().get('name')?.value
// Set cookie
cookies().set('name', 'Delba')
// Delete cookie
cookies().delete('name')
}برای دیدن مثالهای بیشتر در مورد حذف کوکیها از اکشنهای سرور، میتوانید به مستندات مراجعه کنید.
امنیت
احراز هویت و مجوزدهی
باید با اکشنهای سرور مانند نقاط انتهایی (endpoint) یک API عمومی رفتار کنید و اطمینان حاصل کنید که کاربر مجاز به انجام آن کار است. برای مثال:
'use server'
import { auth } from './lib'
export function addItem() {
const { user } = auth()
if (!user) {
throw new Error('You must be signed in to perform this action')
}
// ...
}بستهبندی (Closures) و رمزنگاری
تعریف یک اکشن سرور درون یک کامپوننت، یک بستهبندی closure (opens in a new tab) ایجاد میکند که در آن اکشن به محدوده (scope) تابع بیرونی دسترسی دارد. برای مثال، اکشن publish به متغیر publishVersion دسترسی دارد:
export default async function Page() {
const publishVersion = await getLatestVersion();
async function publish() {
"use server";
if (publishVersion !== await getLatestVersion()) {
throw new Error('The version has changed since pressing publish');
}
...
}
return (
<form>
<button formAction={publish}>Publish</button>
</form>
);
}بستهبندیها زمانی مفید هستند که نیاز داشته باشید یک snapshot از دادهها (برای مثال publishVersion) را در زمان رندر گرفتن کنید تا بعداً زمانی که اکشن فراخوانده شد، قابل استفاده باشد.
با این حال، برای اینکه این اتفاق بیفتد، متغیرهای گرفته شده (captured) هنگام فراخوانی اکشن به کلاینت و سپس به سرور ارسال میشوند. برای جلوگیری از افشای دادههای حساس به کلاینت، Next.js به طور خودکار متغیرهای بستهبندی شده را رمزنگاری میکند. یک کلید خصوصی جدید برای هر اکشن هر بار که یک برنامه Next.js ساخته میشود، تولید میشود. این بدان معناست که اکشنها فقط برای یک build خاص قابل فراخوانی هستند.
خوب است بدانید ما توصیه نمیکنیم تنها به رمزنگاری برای جلوگیری از افشای مقادیر حساس در کلاینت تکیه کنید. در عوض، باید از React taint APIs برای جلوگیری پیشگیرانه از ارسال دادههای خاص به کلاینت استفاده کنید.
بازنویسی کلیدهای رمزنگاری (پیشرفته)
هنگام میزبانی خود (self-hosting) برنامه Next.js در چندین سرور، ممکن است هر نمونه سرور به یک کلید رمزنگاری متفاوت ختم شود که منجر به ناسازگاریهای احتمالی میشود.
برای کاهش این ریسک، میتوانید کلید رمزنگاری را با استفاده از متغیر محیطی process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY بازنویسی کنید. مشخص کردن این متغیر تضمین میکند که کلیدهای رمزنگاری شما در سراسر buildها ثابت میمانند و همه نمونههای سرور از یک کلید یکسان استفاده میکنند.
این یک سناریوی پیشرفته است که در آن رفتار رمزنگاری سازگار در چندین استقرار برای برنامه شما حیاتی است. شما باید رویههای امنیتی استاندارد مانند چرخش کلید و امضا را در نظر بگیرید.
خوب است بدانید: برنامههای Next.js که در Vercel مستقر شدهاند به طور خودکار این کار را مدیریت میکنند.
ریشههای مجاز Allowed origins (پیشرفته)
از آنجایی که اکشنهای سرور را میتوان در یک المنت <form> فراخوانی کرد، این آنها را در معرض حملات CSRF (opens in a new tab) قرار میدهد.
در پشت صحنه، اکشنهای سرور از متد POST استفاده میکنند و فقط این متد HTTP مجاز است آنها را فراخوانی کند. این کار از اکثر آسیبپذیریهای CSRF در مرورگرهای مدرن جلوگیری میکند، به خصوص با پیشفرض بودن کوکیهای SameSite (opens in a new tab).
به عنوان یک حفاظت اضافی، اکشنهای سرور در Next.js همچنین هدر Origin (opens in a new tab) را با هدر Host (opens in a new tab) (یا X-Forwarded-Host) مقایسه میکنند. اگر اینها مطابقت نداشته باشند، درخواست لغو خواهد شد. به عبارت دیگر، اکشنهای سرور فقط میتوانند در همان میزبان صفحهای که آن را میزبانی میکند، فراخوانی شوند.
برای برنامههای بزرگ که از پروکسیهای معکوس یا معماریهای پشتیبان چند لایهای (جایی که API سرور با دامنه تولید متفاوت است) استفاده میکنند، توصیه میشود از گزینه پیکربندی serverActions.allowedOrigins برای مشخص کردن لیستی از ریشههای مجاز استفاده کنید. این گزینه یک آرایه از رشتهها را میپذیرد.
/** @type {import('next').NextConfig} */
module.exports = {
experimental: {
serverActions: {
allowedOrigins: ['my-proxy.com', '*.my-proxy.com'],
},
},
}برای اطلاعات بیشتر در مورد امنیت و اکشنهای سرور (opens in a new tab)، میتوانید به مستندات زیر مراجعه کنید:
منابع اضافی
برای اطلاعات بیشتر در مورد اکشنهای سرور، به مستندات React زیر مراجعه کنید: