"عندك Server Action وعايز تبعت ID. في تلات طرق تعمل ده—اتنين منهم غلط. هنا الطريقة الصح، وليه ده مهم."
المشكلة اللي بتحصل من أول يوم
بتبني فورم تعديل. عندك يوزر، منتج، بوست—مش مهم. محتاج تعمل update. فبتكتب Server Action وبتفكر: "إزاي أبعت الـ ID؟"
أول فكرة بتيجي في بالك؟ Hidden input.
// ❌ أول فكرة بتيجي في بالك
export default function EditForm({ user }: { user: User }) {
return (
<form action={updateUser}>
<input type="hidden" name="id" value={user.id} />
<input name="username" defaultValue={user.name} />
<button type="submit">تحديث</button>
</form>
)
}
بتشتغل. بتتنفذ. مفيش errors. فبتعمل deploy.
بس في الحقيقة إيه اللي عملته؟ حطيت الـ ID في الـ DOM. أي حد يفتح DevTools، يغير الـ value، ويعمل submit بـ ID تاني. لو الـ Server Action بتاعك مش بتتحقق من الصلاحيات صح، يبقى إنت كده فتحت الباب لأي حد يعدل على بيانات مش بتاعته.
عشان كده hidden inputs هي الإجابة الغلط.
التلات طرق لتمرير الـ ID
في تلات طرق. واحدة وحشة، واحدة ماشية، وواحدة هي الإجابة الصح.
الطريقة الأولى: Hidden Input ❌
شفتها. بتشتغل بس بتحط الـ ID في الـ DOM. متستخدمهاش إلا لو عندك سبب جداً محدد—وحتى وقتها، فكر مرتين.
الطريقة التانية: .bind() ✅
// ✅ أحسن
export default function EditForm({ user }: { user: User }) {
const updateUserWithId = updateUser.bind(null, user.id)
return (
<form action={updateUserWithId}>
<input name="username" defaultValue={user.name} />
<button type="submit">تحديث</button>
</form>
)
}
// actions.ts
'use server'
export async function updateUser(id: string, formData: FormData) {
// 👆 جاية من .bind()
// 👆 بيانات الفورم
const username = formData.get('username') as string
await db.users.update({ where: { id }, data: { username } })
}
الـ ID ما بيلمسش الـ DOM خالص. بيتربط بالـ function قبل ما الفورم يتعرض أصلاً. نضيف.
ده اللي Next.js docs بتوصي بيه. وده تحسين حقيقي على hidden inputs.
بس لسه ناقصه حاجات. مفيش loading state. مفيش error handling. مفيش طريقة تورّي للمستخدم إيه اللي حصل. هتحتاج تعمل كل ده بنفسك مع useState.
الطريقة التالتة: useActionState + .bind() ✅✅
ده هو الإجابة الصح. useActionState بتاع React 19 بيديك كل اللي .bind() بيديك إياه، زائد loading state وحالة الـ action—مدمجين جوا.
// ✅ الطريقة الصح
'use client'
import { useActionState } from 'react'
import { updateUser } from './actions'
export default function EditForm({ user }: { user: User }) {
const updateUserWithId = updateUser.bind(null, user.id)
const [state, formAction, isPending] = useActionState(updateUserWithId, null)
return (
<form action={formAction}>
<input name="username" defaultValue={user.name} />
{state?.error && (
<p style={{ color: 'red' }}>{state.error}</p>
)}
{state?.success && (
<p style={{ color: 'green' }}>اتحدث بنجاح!</p>
)}
<button type="submit" disabled={isPending}>
{isPending ? 'جاري الحفظ...' : 'حفظ التغييرات'}
</button>
</form>
)
}
// actions.ts
'use server'
export async function updateUser(id: string, prevState: unknown, formData: FormData) {
// 👆 .bind() 👆 useActionState 👆 بيانات الفورم
try {
const username = formData.get('username') as string
if (!username) {
return { success: false, error: 'اسم المستخدم مطلوب' }
}
await db.users.update({ where: { id }, data: { username } })
return { success: true, error: null }
} catch {
return { success: false, error: 'حصل حاجة غلط' }
}
}
مفيش useState للـ loading. مفيش useState للـ errors. مفيش useState للـ success. كلها موجودة.
ترتيب الـ Arguments: ماتغلطش فيه
هنا أغلب الناس بتتخبط. لما بتجمع .bind() مع useActionState، ترتيب الـ arguments في الـ action ثابت:
(boundArgs..., prevState, formData)
useActionState دايماً بتحقن prevState قبل formData. بعد أي bound args.
// مع .bind(null, user.id):
export async function updateUser(
id: string, // 1. من .bind()
prevState: unknown, // 2. من useActionState — دايماً هنا
formData: FormData // 3. بيانات الفورم — دايماً الأخير
) {}
إقلّب prevState وid وكل حاجة هتبوظ. بصمت. مفيش TypeScript error. بس سلوك غلط.
اعمل الترتيب صح من أول مرة.
إيه اللي useActionState بيديك بلاش
خليني أكون واضح ليه الـ pattern ده بيكسب:
isPending — ما بتكتبش سطر واحد من كود الـ loading state. الـ hook بيعملها. الزرار بيـ disable لما الـ action بتتنفذ. خلاص.
state — أي حاجة الـ action بتاعتك بترجّعها بتبقى state. رجّعت error؟ وريها. رجّعت success؟ وري ده. الـ component بيـ re-render أوتوماتيك.
Progressive enhancement — الفورم بيشتغل من غير JavaScript. useActionState مش بتكسره. لو الـ JS اتحمل، المستخدم بياخد التجربة الكاملة. لو ما اتحملش، الفورم لسه بيشتغل.
مفيش DOM exposure — الـ ID بيتربط قبل الـ render. ما بيظهرش في الـ HTML خالص. المستخدمين مش قادرين يتلاعبوا بيه من DevTools.
مثال حقيقي: فورم تعديل منتج
هنا مثال كامل جاهز للـ production:
// app/products/[id]/edit/page.tsx
import { getProductById } from '@/lib/dal/products'
import EditProductForm from '@/app/components/edit-product-form'
export default async function EditProductPage({
params
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const product = await getProductById(id)
return (
<div>
<h1>تعديل المنتج</h1>
<EditProductForm product={product} />
</div>
)
}
// app/components/edit-product-form.tsx
'use client'
import { useActionState } from 'react'
import { updateProduct } from '@/app/actions'
import type { ProductDTO } from '@/lib/dal/types'
export default function EditProductForm({ product }: { product: ProductDTO }) {
const updateProductWithId = updateProduct.bind(null, product.id)
const [state, formAction, isPending] = useActionState(updateProductWithId, null)
return (
<form action={formAction}>
<div>
<label htmlFor="name">اسم المنتج</label>
<input
id="name"
name="name"
defaultValue={product.name}
required
/>
</div>
<div>
<label htmlFor="price">السعر</label>
<input
id="price"
name="price"
type="number"
defaultValue={product.price}
required
/>
</div>
{state?.error && (
<p role="alert" style={{ color: 'red' }}>{state.error}</p>
)}
{state?.success && (
<p role="status" style={{ color: 'green' }}>تم تحديث المنتج!</p>
)}
<button type="submit" disabled={isPending}>
{isPending ? 'جاري الحفظ...' : 'حفظ التغييرات'}
</button>
</form>
)
}
// app/actions.ts
'use server'
import { revalidateTag } from 'next/cache'
import { updateProduct } from '@/lib/dal/products'
export async function updateProduct(
id: string,
prevState: unknown,
formData: FormData
) {
try {
const name = formData.get('name') as string
const price = Number(formData.get('price'))
if (!name) return { success: false, error: 'الاسم مطلوب' }
if (isNaN(price)) return { success: false, error: 'السعر مش صح' }
await updateProduct(id, { name, price })
revalidateTag('products', 'max')
revalidateTag(`product-${id}`, 'max')
return { success: true, error: null }
} catch {
return { success: false, error: 'فشل تحديث المنتج' }
}
}
Server Component نضيف. فورم Client نضيف. Server Action نضيف. كل حاجة في مكانها.
إيه اللي المفروض تستخدمه؟
Hidden Input |
|
| |
|---|---|---|---|
الـ ID في الـ DOM؟ | ❌ مكشوف | ✅ مخفي | ✅ مخفي |
Loading state | ❌ يدوي | ❌ يدوي | ✅ مدمج |
القاعدة بسيطة:
محتاج feedback (errors، loading، success)? →
useActionState+.bind()Action بسيطة جداً من غير أي feedback? →
.bind()لوحده كويسHidden input? → لأ. بجد لأ.
في التطبيقات الحقيقية، دايماً محتاج feedback. فـ useActionState + .bind() بتكسب تقريباً كل مرة.
الخلاصة
Hidden inputs حاسس إنها مريحة. مش كده. بتكشف بيانات مش المفروض تكون في الـ DOM وبتسيبك تعمل loading وerror state بإيدك.
.bind() + useActionState هو الـ pattern اللي React 19 وNext.js اتبنوا عشانه. الـ ID بيفضل بعيد عن الـ DOM. الـ loading state مجاني. الـ error handling مدمج.
ابدأ بالـ pattern ده من أول يوم. نفسك في المستقبل—والمستخدمين بتوعك—هيشكروك.
المصادر:
ترميز سعيد! — Ahmed Fahmy