Next.js

إجراءات خادم Next.js والتخزين المؤقت المتقدم في App Router

أتقن إجراءات خادم Next.js، وواجهة المستخدم التفاؤلية، واستراتيجيات التخزين المؤقت المتقدمة مثل revalidateTag في App Router لتطبيقات سريعة ومتناسقة البيانات.

By Mohamed DjoudirMay 16, 202610 min read
مشاركة:
إجراءات خادم Next.js والتخزين المؤقت المتقدم في App Router
#next.js#server-actions#caching#revalidation#app-router#performance

تتطلب تطبيقات الويب الحديثة، وخاصة منصات SaaS ولوحات التحكم (dashboards)، كلاً من سرعة الاستجابة واتساق البيانات. توفر Next.js Server Actions، جنباً إلى جنب مع استراتيجيات التخزين المؤقت (caching) وإعادة التحقق (revalidation) المتطورة داخل App Router، أدوات قوية لتحقيق ذلك. ومن خلال فهم كيفية الاستفادة من هذه الميزات، يمكن للمطورين بناء تجارب عالية الأداء وسهلة الاستخدام مع تبسيط التفاعلات مع الخلفية (backend).

إتقان Next.js Server Actions لتعديل البيانات بسلاسة

تبسط Server Actions عملية إجراء التغييرات على الخادم مباشرة من مكونات React الخاصة بك. بدلاً من بناء مسارات API منفصلة لمنطق التطبيق الداخلي، يمكنك تحديد وظائف برمجية من جهة الخادم واستدعاؤها مباشرة من Client Components أو النماذج (forms). يقلل هذا بشكل كبير من الأكواد المتكررة (boilerplate) ويحسن تجربة المطور.

تستخدم Server Actions بشكل أساسي طلبات POST وهي مثالية لتعديلات البيانات (data mutations) مثل إنشاء السجلات أو تحديثها أو حذفها. كما أنها توفر "التحسين التدريجي" (progressive enhancement)، مما يعني أن النماذج ستعمل حتى لو تم تعطيل JavaScript في المتصفح، مما يوفر تجربة أساسية قوية. بالنسبة لجلب البيانات العام أو الكشف عن نقاط نهاية HTTP عامة، غالباً ما تظل Route Handlers (مسارات API) هي الخيار الأنسب.

يقدم React 19 خطافات (hooks) منقحة، مدعومة بالكامل في Next.js 16، تتكامل بسلاسة مع Server Actions:

  1. useActionState: تطور لـ useFormState؛ يوفر هذا الخطاف إدارة شاملة للحالة لدورة حياة التعديل بالكامل، بما في ذلك بيانات النموذج، وحالة الانتظار، والنتائج أو الأخطاء القادمة من الخادم.
  2. useOptimistic: يتيح تحديثات فورية لواجهة المستخدم قبل اكتمال التعديل على الخادم، مما يعزز الأداء المدرك وتجربة المستخدم.
  3. useFormStatus: يوفر وصولاً دقيقاً إلى حالة الانتظار لأقرب عنصر <form>، مما يسمح بتقديم ملاحظات مرئية مثل تعطيل الأزرار أو عرض مؤشرات التحميل.

إليك مثال أساسي لـ Server Action:

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

import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";

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

  // Simulate a database call
  await new Promise((resolve) => setTimeout(resolve, 1000));
  console.log("Creating post:", { title, content });

  // Invalidate cache to show new data
  revalidatePath("/blog");
  redirect("/blog");
}

وكيفية استخدامه في Client Component:

// app/blog/create/page.tsx
"use client";

import { createPost } from "@/app/actions";
import { useFormStatus } from "react-dom";

function SubmitButton() {
  const { pending } = useFormStatus();
  return (
    <button type="submit" disabled={pending}>
      {pending ? "Creating..." : "Create Post"}
    </button>
  );
}

export default function CreatePostPage() {
  return (
    <form action={createPost}>
      <input type="text" name="title" placeholder="Title" required /&gt;
      <textarea name="content" placeholder="Content" required /&gt;
      <SubmitButton />
    </form>
  );
}

إتقان التخزين المؤقت في Next.js 16 باستخدام Cache Components

يقدم Next.js 16 نموذج تخزين مؤقت (caching) أكثر وضوحاً وقوة داخل App Router، يتمحور حول Cache Components. عند ضبط cacheComponents: true في ملف next.config.ts الخاص بك، تصبح جميع Server Components عبارة عن Cache Components. هذا التحول الجذري يعني أنه بشكل افتراضي، يتم تشغيل الأكواد الديناميكية داخل Cache Components في وقت الطلب (request time)، مما يمنحك تحكماً دقيقاً في وقت وكيفية تخزين البيانات مؤقتاً.

إليك كيفية عمل نموذج التخزين المؤقت الجديد هذا:

  • ديناميكي افتراضياً: مع تمكين Cache Components، يكون جلب البيانات داخل Server Components ديناميكياً بشكل افتراضي. يضمن ذلك الحصول على بيانات حديثة لكل طلب ما لم يتم تخزينها مؤقتاً بشكل صريح.
  • التخزين المؤقت الصريح باستخدام "use cache": يمكنك اختيار تخزين Server Components معينة أو وظائف جلب البيانات مؤقتاً باستخدام التوجيه "use cache". يخبر هذا التوجيه Next.js بتخزين مخرجات ذلك المكون أو تلك الوظيفة.
  • cacheTag و cacheLife للتحكم:
    • cacheTag(...tags: string[]): يخصص علامة (tag) واحدة أو أكثر للبيانات المخزنة مؤقتاً، مما يتيح لك إبطالها بشكل انتقائي لاحقاً باستخدام updateTag أو revalidateTag.
    • cacheLife({ stale, revalidate, expire }): يحدد بشكل صريح ملف تعريف انتهاء الصلاحية للبيانات المخزنة مؤقتاً. بينما يوفر Next.js إعدادات مسبقة نصية (مثل "hours" أو "days") ، يُوصى باستخدام شكل الكائن الصريح لأن الإعدادات الافتراضية قد تختلف حسب البيئة.
  • Router Cache (جهة العميل): لا يزال Router Cache من جهة العميل موجوداً، حيث يخزن النتائج المعروضة للمسارات التي تمت زيارتها للتنقل الفوري. وهو يعمل جنباً إلى جنب مع Cache Components من جهة الخادم.
  • Request Memoization: داخل طلب خادم واحد، إذا تم جلب نفس البيانات عدة مرات، يقوم Next.js بحفظ النتيجة (memoizes) وإعادة استخدامها لمنع العمليات المتكررة.

يمكّن هذا النموذج الصريح المطورين من اتخاذ قرارات مدروسة بشأن حداثة البيانات والأداء، بدلاً من الاعتماد على سلوكيات التخزين المؤقت الضمنية.

إعادة التحقق بدقة: الاستفادة من updateTag و revalidateTag و revalidatePath

عندما تتغير بياناتك، يصبح ضمان رؤية المستخدمين للمعلومات المحدثة أمراً بالغ الأهمية. يوفر Next.js طرقاً قوية لإبطال البيانات المخزنة مؤقتاً. وفهم متى يجب استخدام كل منها هو المفتاح لإدارة فعالة للتخزين المؤقت.

updateTag(tag: string): إبطال فوري لضمان "قراءة ما كتبت"

هذا هو واجهة برمجة التطبيقات (API) الموصى بها لـ Server Action فقط للإبطال الفوري بعد تعديل البيانات. عندما تستدعي updateTag(tag)، يقوم Next.js بإبطال جميع البيانات المخزنة مؤقتاً المرتبطة بتلك العلامة على الفور، مما يضمن أن القراءات اللاحقة تعكس أحدث التغييرات. هذا أمر حيوي لاتساق "قراءة ما كتبت" (read-your-writes consistency) في التطبيقات حيث يتوقع المستخدمون رؤية تغييراتهم تنعكس فوراً.

"use server";
import { updateTag } from "next/cache";

export async function createProduct(formData: FormData) {
  // ... database logic to create product ...
  updateTag("products"); // Immediately invalidate product-related data
}

revalidateTag(tag: string, profile: string | { expire: number }): Stale-While-Revalidate

تُستخدم revalidateTag() لتكوين سلوك stale-while-revalidate للبيانات المخزنة مؤقتاً. وهي تعمل عن طريق تعيين ملف تعريف انتهاء صلاحية جديد للبيانات المرتبطة بعلامة معينة.

تحذير: تم إيقاف استخدام صيغة الوسيط الواحد revalidateTag(tag) في Next.js 16. يجب التحديث دائماً إلى الصيغة ذات الوسيطين.

عندما تجلب البيانات، يمكنك ربط علامة بها:

async function getProducts() {
  const res = await fetch("https://api.example.com/products", {
    next: { tags: ["products"] },
  });
  return res.json();
}

بعد ذلك، يمكنك إعادة التحقق منها باستخدام ملف تعريف مدمج (مثل "max") أو انتهاء صلاحية مخصص:

"use server";
import { revalidateTag } from "next/cache";

export async function periodicallyUpdateProducts() {
  // Sets the cache to serve stale content while revalidating in the background.
  // The "max" profile is recommended for long-lived stale-while-revalidate data.
  revalidateTag("products", "max");
}

الفرق الرئيسي: تفرض updateTag إعادة جلب فورية للبيانات المتأثرة، بينما تحدد revalidateTag جدولاً زمنياً لمتى تصبح البيانات قديمة (stale) ومؤهلة لإعادة التحقق.

revalidatePath(path: string): الإبطال المستند إلى المسار

تقوم هذه الوظيفة بإعادة التحقق من جميع البيانات المخزنة مؤقتاً خصيصاً لمسار معين (مثل /blog) وتعمل أيضاً على تحديث Full Route Cache لهذا المسار. ورغم أنها أبسط في الاستخدام، إلا أنها قد تكون أقل كفاءة من updateTag إذا تغير جزء صغير فقط من بيانات الصفحة، مما قد يتسبب في عمليات إعادة جلب غير ضرورية للمكونات الأخرى على ذلك المسار.

في Server Functions، تقوم revalidatePath حالياً أيضاً بتحديث الصفحات التي تمت زيارتها مسبقاً، ولكن هذا السلوك مؤقت بشكل صريح. بالنسبة لمعظم السيناريوهات التي تتضمن أنواع بيانات محددة (مثل قائمة المقالات أو ملفات تعريف المستخدمين)، تقدم updateTag أو revalidateTag حلاً أكثر قوة وكفاءة.

تحسين تجربة المستخدم مع Optimistic UI وحالات الانتظار في Server Actions

جانب حاسم في تجربة المستخدم الجيدة هو تقديم ملاحظات فورية. تجعل Server Actions، مع خطافات React 19، تنفيذ Optimistic UI وإدارة حالات الانتظار أمراً مباشراً:

  1. التحديثات المتفائلة (Optimistic Updates) مع useOptimistic: يتيح لك هذا الخطاف تحديث واجهة المستخدم فوراً بنتيجة مفترضة للتعديل، مع التراجع عنها أو تأكيدها بمجرد وصول استجابة الخادم الفعلية. وهذا يجعل التطبيق يبدو سريعاً للغاية.

    // Client Component
    "use client";
    import { useOptimistic } from "react";
    import { updateItem } from "@/app/actions";
    
    type Item = { id: string; text: string; completed: boolean };
    
    export function TodoList({ items }: { items: Item[] }) {
      const [optimisticItems, addOptimisticItem] = useOptimistic(
        items,
        (currentItems, updatedItem: Item) => {
          const itemIndex = currentItems.findIndex((i) => i.id === updatedItem.id);
          if (itemIndex === -1) return [...currentItems, updatedItem];
          return currentItems.map((item) =>
            item.id === updatedItem.id ? { ...item, ...updatedItem } : item
          );
        }
      );
    
      async function toggleTodo(item: Item) {
        addOptimisticItem({ ...item, completed: !item.completed });
        await updateItem({ ...item, completed: !item.completed });
      }
    
      return (
        <ul>
          {optimisticItems.map((item) => (
            <li key={item.id}>
              <input
                type="checkbox"
                checked={item.completed}
                onChange={() => toggleTodo(item)}
              />
              {item.text} {item.completed ? "(Optimistic)" : ""}
            </li>
          ))}
        </ul>
      );
    }
    
  2. حالات الانتظار مع useFormStatus: استخدم هذا الخطاف داخل زر الإرسال <button type="submit"> أو مكون فرعي لإظهار مؤشرات التحميل أو تعطيل المدخلات أثناء تنفيذ الإجراء.

    // (راجع مثال SubmitButton في قسم Server Actions)
    
  3. دورة حياة شاملة مع useActionState: بالنسبة للنماذج، يعد useActionState قوياً لأنه يجمع بين وظائف إدارة بيانات النموذج ونتيجة الإجراء وحالة الانتظار، مما يوفر نهجاً موحداً للتعامل مع تعديلات الخادم وتفاعلات واجهة المستخدم الخاصة بها.

من خلال دمج هذه الخطافات، يمكنك إنشاء واجهات مستخدم تفاعلية وعالية الأداء تستجيب فوراً لمدخلات المستخدم مع ضمان اتساق البيانات مع الخلفية.

أنماط التخزين المؤقت المتقدمة: الاستفادة من "use cache" وإعادة التحقق الموزعة

بينما يغطي fetch و updateTag العديد من احتياجات التخزين المؤقت، يقدم Next.js أنماطاً أكثر تقدماً لسيناريوهات محددة:

توجيه "use cache" دقيق التحكم

مع ضبط cacheComponents: true في next.config.ts ، يمكنك الاستفادة من توجيه "use cache" داخل Server Components أو وظائف جلب البيانات. يسمح هذا بإعادة التحقق بدقة استناداً إلى الوقت باستخدام cacheLife:

// server-only.ts
import { cacheLife } from "next/cache";

export async function getData() {
  "use cache";
  
  // يُفضل تكوين الكائن الصريح على الإعدادات المسبقة النصية
  // لضمان سلوك متسق عبر البيئات المختلفة.
  cacheLife({
    stale: 3600,      // ساعة واحدة حتى تُعتبر قديمة على العميل
    revalidate: 7200, // ساعتان حتى تتم إعادة التحقق على الخادم
    expire: 86400     // يوم واحد حتى تنتهي صلاحيتها تماماً
  });
  
  const res = await fetch("https://api.example.com/some-data");
  return res.json();
}

يوفر هذا تحكماً دقيقاً في عمليات جلب البيانات الفردية أو عرض المكونات، وهو مثالي للبيانات التي لا تتغير بشكل متكرر ولكنها لا تزال بحاجة إلى تحديثات دورية.

إعادة التحقق الموزعة لعمليات النشر متعددة النسخ

في عمليات نشر Next.js متعددة النسخ (الشائعة في بيئات الإنتاج)، غالباً ما تكون أحداث إبطال التخزين المؤقت التي يتم تشغيلها بواسطة updateTag أو revalidatePath محلية للنسخة التي عالجت الطلب. وهذا يعني أنه إذا قام مستخدم بتحديث البيانات على نسخة خادم واحدة، فقد تظل النسخ الأخرى تقدم محتوى قديماً حتى تنتهي صلاحية مخازنها المحلية بشكل طبيعي أو يتم إعادة التحقق منها أيضاً.

لضمان اتساق البيانات عبر جميع النسخ، تحتاج إلى تنفيذ آلية لنشر نداءات إبطال التخزين. يتضمن هذا عادةً تكوين cacheHandlers مخصصة في next.config.ts تبث رسائل الإبطال هذه إلى جميع النسخ قيد التشغيل. بينما يوفر Next.js الخطافات اللازمة لـ cache handlers المخصصة للأعمال المتقدمة، فإنه لا يتضمن وصفة مدمجة للبث عبر مجموعة من الخوادم؛ يترك هذا لبنيتك التحتية.

اعتبارات جلب البيانات من جهة العميل

حتى مع وجود التخزين المؤقت القوي المدمج في Server Components وواجهة برمجة تطبيقات fetch الأصلية الموسعة، لا تزال مكتبات جلب البيانات من جهة العميل مثل SWR أو React Query تقدم فوائد كبيرة، خاصة لـ:

  1. أنماط إبطال التخزين المؤقت المعقدة جهة العميل: عندما تتطلب تفاعلات المستخدم داخل Client Component إدارة متطورة للتخزين المؤقت.
  2. التحديثات المتفائلة المتقدمة: لواجهات المستخدم شديدة التفاعلية حيث تكون تعديلات الحالة المحلية معقدة.
  3. حالات واجهة المستخدم المخصصة للتحميل والخطأ والنجاح: غالباً ما توفر هذه المكتبات طرقاً أكثر تنظيماً وانسيابية لإدارة هذه الحالات في Client Components.

بينما تتفوق Server Components في جلب البيانات الأولية والتعديلات، تظل هذه المكتبات أدوات قيمة للتفاعل الغني من جهة العميل.

خاتمة

إن Next.js Server Actions، جنباً إلى جنب مع الفهم العميق لآليات التخزين المؤقت في App Router واستراتيجيات إعادة التحقق المتقدمة، تمكنك من بناء تطبيقات ويب عالية الأداء وسريعة الاستجابة ومتسقة البيانات. من خلال إتقان أدوات مثل useActionState و useOptimistic و updateTag و "use cache"، يمكنك تقديم تجارب مستخدم استثنائية وتبسيط سير عمل التطوير الخاص بك. وبينما تقوم بتطوير تطبيقات Next.js الخاصة بك، تذكر أن Aniq UI تقدم قوالب متميزة وجاهزة للإنتاج تم بناؤها بهذه الممارسات الفضلى، مما يساعدك على الإطلاق بشكل أسرع وبثقة.

هل وجدت هذا المقال مفيدًا؟

مشاركة:
world map

مجتمع المستخدمين العالمي

انضم إلى آلاف المطورين في جميع أنحاء العالم الذين يثقون بـ Aniq-UI لمشاريعهم. يتم استخدام قوالبنا في جميع أنحاء العالم لإنشاء تجارب ويب مذهلة.

تواصل معنا

Need custom work or reskin? Get in touch with us

Aniq-uiAniq-uiAniq-ui