لماذا أفضل الدرج على الصفحة الكاملة
عن استخدام الدرج كواجهة مستخدم تنفيذية في مواقع التجارة الإلكترونية، يُناقش من منظور تجربة المستخدم، هيكلية الحالة، TypeScript، إمكانية الوصول، وتجربة الهاتف المحمول التي تبدو طبيعية.
ملاحظات حول واجهة المستخدم التنفيذية في مواقع التجارة الإلكترونية (مع TypeScript)
لقد مضى وقت طويل منذ أن كتبت مقالًا تقنيًا. ليس لأنني نفدت من المواضيع،
بل لأنني أشعر مؤخرًا أن العديد من المواضيع التقنية المثيرة لا تُناقش بصدق كافية — خاصة فيما يتعلق بقرارات واجهة المستخدم التي تبدو "صغيرة"،
لكن تأثيرها يمتد إلى تجربة المستخدم وبنية الكود.
واحدة من هذه القرارات هي اختيار الدرج بدلاً من الصفحة الكاملة في سياق التجارة الإلكترونية.
هذا المقال ليس دعوة للتخلي عن الصفحة الكاملة تمامًا. هو أشبه بملاحظات شخصية: لماذا، في كثير من حالات التجارة الإلكترونية، أشعر أن الدرج أكثر توافقًا مع واجهة المستخدم التنفيذية،
وكيف تؤثر هذه القرارات على طريقة كتابتي لـ TypeScript وإدارة حالة التطبيق.
الصفحة الكاملة ليست مشكلة، لكنها غالبًا غير مناسبة
الصفحة الكاملة هي النهج الافتراضي في الويب. وهذا ليس أمرًا سيئًا.
مناسب لـ:
• المقالات.
• صفحات الهبوط.
• الصفحات المعلوماتية.
• المحتوى الطويل الذي يُراد قراءته بتمعن.
الصفحة الكاملة غالبًا هي الخيار الأفضل.
ما يبدأ في الشعور بأنه "غير ملائم" هو عندما يُنقل نمط الصفحة الكاملة بشكل مباشر إلى التجارة الإلكترونية، خصوصًا للتفاعلات السريعة والمتكررة.
مثال بسيط:
• المستخدم يتصفح قائمة المنتجات.
• ينقر على منتج.
• ينتقل إلى صفحة التفاصيل.
• يعود إلى الصفحة السابقة.
• يعيد التمرير.
• يكرر نفس العملية.
تقنيًا هذا صحيح.
من ناحية تجربة المستخدم... غالبًا ما يشعر المستخدم بأن هناك احتكاكًا غير ضروري.
الدرج ليس بديلاً للصفحة، بل طبقة تفاعل
الخطأ الشائع عند الحديث عن الدرج هو اعتباره بديلاً للصفحة. في الواقع، الدرج أراه كطبقة تفاعل.
الدرج مناسب لـ:
• تفاصيل المنتج (عرض سريع).
• عربة التسوق.
• الدفع السريع.
• الدردشة / التواصل.
• معاينة المعلومات.
الصفحة الكاملة تظل مهمة لـ:
• تحسين محركات البحث (SEO).
• روابط قابلة للمشاركة.
• القراءة المتعمقة.
لذا، الأمر ليس "الدرج مقابل الصفحة" كخيارات متعارضة. بل متى نحتاج إلى التنقل، ومتى نحتاج إلى تنفيذ المعاملات.
واجهة المستخدم التنفيذية مقابل واجهة المستخدم التنقلية
في هذه النقطة أجد أنه من المفيد التمييز بين نوعي واجهة المستخدم:
واجهة المستخدم التنقلية.
• الهدف الرئيسي: الاستكشاف.
• المستخدم "يتجول" من صفحة إلى أخرى.
• مثال: المدونات، المقالات، صفحة التعريف.
واجهة المستخدم التنفيذية.
• الهدف الرئيسي: إنجاز نية المستخدم.
• المستخدم يعرف ما يريد فعله.
• مثال: إضافة إلى العربة، اختيار المتغيرات، الدفع.
الدرج يتفوق عادة في الواجهة التنفيذية لأنه:
• يقلل تبديل السياق.
• يقدم أداءً محسوسًا أسرع.
• يركز على الفعل، لا الاستكشاف.
ببساطة: في التجارة الإلكترونية، المستخدم غالبًا لا يريد "تغيير الصفحة"، بل يريد إنجاز نية.
المشكلة الشائعة: درج غير "مُطبّق بنمط Typed"
العديد من تطبيقات الدرج تفشل ليس بسبب واجهة المستخدم، بل لأن الحالة لم تُصمم بشكل صحيح. الدرج غالبًا ما يُعامل كالتالي:
const [isOpen, setIsOpen] = useState(false);
ثم يتوسع إلى:
• isCartOpen
• isProductOpen
• isCheckoutOpen
ويتحول تدريجيًا إلى جحيم القيم البوليانية.
إذا كان التطبيق صغيرًا، قد يبدو الأمر آمنًا. لكن مع زيادة الميزات ونقاط الدخول، الحالة التي "مجرد بوليان" تصبح صعبة التنبؤ بسلوكها.
الدرج كحالة، وليس مجرد مكون واجهة مستخدم (TypeScript)
النهج الذي أراه أكثر صحة هو اعتبار الدرج كحالة صريحة ومُطبقة بنمط.
مثال بسيط بـ TypeScript:
type DrawerState =
| { key: "PRODUCT_DETAIL"; payload: { productId: string } }
| { key: "CART" }
| { key: "CHECKOUT" }
| { key: null };
لماذا أحب هذا النموذج:
• عند فتح PRODUCT_DETAIL، يجب وجود الـ payload.
• عند فتح CART، الـ payload غير مطلوب (ويفضل عدم فرضه).
• الدرج لا يملك "حالة معلقة": مفتوح لكن البيانات مجهولة.
هنا TypeScript لا تساعد فقط في "الإكمال التلقائي"، بل تحافظ على وضوح عقد سلوك التطبيق.
هيكل الدرج الذي أعتبره "صحيًا"
أفضل هيكل كهذا:
src/
features/
drawer/
drawer.types.ts
drawer.store.ts
DrawerRoot.tsx
drawers/
ProductDetailDrawer.tsx
CartDrawer.tsx
CheckoutDrawer.tsx
• DrawerRoot مسؤول فقط عن عرض الدرج حسب الحالة.
• كل درج منفصل حسب الميزة.
• drawer.store.ts يحتوي الحالة والإجراءات.
• drawer.types.ts يحتوي نوع الاتحاد (جوهر أمان النوع).
هذا الهيكل غير مبهر، لكنه عادة ما يصمد طويلًا.
مخزن الدرج (مثال بسيط باستخدام Zustand)
أنا شخصيًا أجد Zustand مناسبًا لحالة كهذه لأنه:
• لا يحتاج إلى مزود متعدد الطبقات.
• منطق الحالة أسهل في العزل.
• إعادة العرض يمكن أن تكون أكثر دقة مقارنة بـ Context المستخدم "عالميًا" بدون اختيار.
مثال مخزن بسيط:
import { create } from "zustand";
import type { DrawerState } from "./drawer.types";
type DrawerActions = {
openProductDetail: (productId: string) => void;
openCart: () => void;
openCheckout: () => void;
close: () => void;
};
type DrawerStore = DrawerState & DrawerActions;
export const useDrawerStore = create<DrawerStore>((set) => ({
key: null,
openProductDetail: (productId) =>
set({ key: "PRODUCT_DETAIL", payload: { productId } }),
openCart: () => set({ key: "CART" }),
openCheckout: () => set({ key: "CHECKOUT" }),
close: () => set({ key: null }),
}));
ملاحظة صغيرة: هذا ليس "عدة أدراج"، بل درج واحد بأوضاع متعددة. هذا يجعل التحكم أكثر استقرارًا.
DrawerRoot: مكون واحد لكل الأدراج
"use client";
import { useDrawerStore } from "./drawer.store";
import { ProductDetailDrawer } from "./drawers/ProductDetailDrawer";
import { CartDrawer } from "./drawers/CartDrawer";
import { CheckoutDrawer } from "./drawers/CheckoutDrawer";
export function DrawerRoot() {
const state = useDrawerStore();
const close = useDrawerStore((s) => s.close);
if (!state.key) return null;
return (
<div>
{state.key === "PRODUCT_DETAIL" && (
<ProductDetailDrawer
productId={state.payload.productId}
onClose={close}
/>
)}
{state.key === "CART" && <CartDrawer onClose={close} />}
{state.key === "CHECKOUT" && <CheckoutDrawer onClose={close} />}
</div>
);
}
أحب هذا النمط لأن التطبيق بأكمله يمتلك "بوابة" واحدة للدرج. السلوك يصبح متسقًا، والتصحيح أسهل.
كيفية فتح الدرج من البطاقة (دخول مدفوع بالحالة)
من بطاقة العنصر، يكفي استدعاء:
const openProductDetail = useDrawerStore((s) => s.openProductDetail);
<button onClick={() => openProductDetail(product.id)}>
عرض التفاصيل
</button>
لا تغير عنوان URL. سريع. طبيعي.
الدرج الهجين: عندما يلتقي المسار والحالة
هذا الجزء يجعلني أكثر يقينًا: الدرج والتوجيه لا يجب أن يكونا متناقضين.
الفكرة ببساطة:
• المستخدم ينقر على منتج → يفتح الدرج (مدفوع بالحالة).
• المستخدم يفتح الرابط /product/[slug] مباشرة → يفتح نفس الدرج (مدفوع بالمسار).
الواجهة واحدة.
المنطق واحد.
نقطتا الدخول اثنتان.
مثال مشغل عميل:
"use client";
import { useEffect } from "react";
import { useDrawerStore } from "@/features/drawer/drawer.store";
export function OpenProductDrawerOnMount({ productId }: { productId: string }) {
const open = useDrawerStore((s) => s.openProductDetail);
useEffect(() => {
open(productId);
}, [open, productId]);
return null;
}
في صفحة الخادم، فقط عرض:
// مكون خادم
import { OpenProductDrawerOnMount } from "./OpenProductDrawerOnMount";
export default async function Page({ params }: { params: { slug: string } }) {
const product = await getProductBySlug(params.slug);
return <OpenProductDrawerOnMount productId={product.id} />;
}
بهذا:
• رابط المشاركة يبقى فعالًا.
• التحديث لا يزال ممكنًا.
• تجربة المستخدم تبقى عبر الدرج.
وبالنسبة لي، هذه نقطة التقاء بين الويب الأصلي (URL، SEO) وتجربة التطبيقات (الدرج، المعاملات السريعة).
لماذا الدرج يبدو أسرع، رغم أن الخلفية نفسها
الشيء المثير للاهتمام، الدرج غالبًا ما يبدو أسرع، رغم:
• نفس الـ API.
• نفس البيانات.
• نفس طلبات الشبكة.
السبب:
• لا يوجد فك تركيب صفحة كبير.
• سياق المستخدم يبقى سليمًا.
• الانتقالات البصرية أخف.
يشبه مفهوم الواجهة المتفائلة (optimistic UI)، لكن على مستوى التجربة، لا البيانات.
متى تكون الصفحة الكاملة أكثر ملاءمة
من المهم أن نكون صادقين: الدرج ليس الحل لكل شيء.
الصفحة الكاملة أكثر ملاءمة عندما:
• تحسين محركات البحث هو الأولوية.
• المحتوى طويل وسردي.
• روابط المشاركة هي ميزة أساسية.
• هيكل المعلومات معقد.
فرض الدرج على كل شيء قد يضر تجربة المستخدم.
التأثير على قاعدة الكود وقابلية الصيانة
من وجهة نظر المطور، الدرج المصمم بشكل صحيح:
• يقلل تكرار الصفحات.
• يجعل المنطق أكثر قابلية لإعادة الاستخدام.
• يوضح تدفق الحالة.
• يسهل الاختبار.
المهم ليس الدرج نفسه، بل التفكير في الحالة المدفوعة خلفه.
الخاتمة
في التجارة الإلكترونية، المستخدم لا يأتي "لتغيير الصفحة".
يأتي بنية.
ومهمة واجهة المستخدم الجيدة ليست عرض التنقل،
بل مساعدة المستخدم على إنجاز نيته بأقل احتكاك ممكن.
لهذا، في سياق واجهة المستخدم التنفيذية،
أنا غالبًا ما أختار الدرج على الصفحة الكاملة.
ملاحظات تقنية إضافية
أضيف هذا الجزء لأن هناك أمورًا غالبًا ما تُغفل عند الحديث عن الدرج. ليست بسبب الجهل، بل بسبب التركيز على "أن يعمل فقط".
لكن إذا استُخدم الدرج كمركز للمعاملات، فهذه التفاصيل تصبح عوامل حاسمة للجودة.
نمط خاطئ: درج يبدو صحيحًا لكنه يسبب مشاكل
ليس كل درج "صحي". هناك أنماط تبدو منظمة في البداية، لكنها تصعب الأمور مع نمو التطبيق.
- العديد من الأدراج الفيزيائية لميزات متعددة.
كل ميزة لها درج خاص يُستخدم في أماكن متعددة. النتيجة:
• تشتت الحالة.
• سلوك غير متسق.
• صعوبة في ترتيب الأولويات (مثلاً: العربة مقابل تفاصيل المنتج).
أفضل: درج واحد رئيسي، مع أوضاع متعددة.
- فتح الدرج بدون حمولة واضحة.
مثلاً:
openDrawer("PRODUCT_DETAIL");
بدون حمولة، الدرج يعتمد على حالة أخرى "خارج العقد"، وهذا يعرض البيانات لمخاطر التنافر، خاصة عند التحديث أو التبديل السريع.
- الدرج يحل محل كل الصفحات.
وضع كل شيء في الدرج ليس تحسينًا، بل فرط هندسة. المحتوى الطويل يظل أكثر "صدقًا" في الصفحة.
إمكانية الوصول: درج يجب أن يكون متاحًا بشكل لائق
الدرج هو "لوحة تتراكب فوق واجهة المستخدم". لذلك، يتحمل مسؤولية إمكانية الوصول التي غالبًا ما تُغفل.
بعض الأمور التي أراها ضرورية على الأقل:
• حبس التركيز: عندما يفتح الدرج، لا يجب أن يهرب تركيز لوحة المفاتيح إلى العناصر خلفه.
• مفتاح ESC للإغلاق: يجب أن يستطيع المستخدم الإغلاق بدون استخدام الفأرة.
• الدور وسمات ARIA المناسبة:
role="dialog"
aria-modal="true"
• استعادة التركيز: عند إغلاق الدرج، يعود التركيز إلى العنصر الذي فتحه.
ليس هذا من باب المثالية، بل لأن الدرج يُستخدم كمسار للمعاملات؛ لذا يجب أن يكون مريحًا لمجموعة واسعة من المستخدمين.
Context مقابل Zustand للدرج: خيار يبدو بسيطًا لكنه مهم
لا أعتبر Context سيئًا. Context مناسب لكثير من الحالات. لكن للدرج خصائص مميزة: عالمي، يتغير كثيرًا، ويُستخدم في مكونات متعددة.
Context مناسب إذا:
• الدرج بسيط.
• نادراً ما يتغير.
• عدد المستهلكين قليل.
• التطبيق صغير.
لكن Context يصبح ثقيلًا إذا:
• الدرج يتغير كثيرًا (فتح/إغلاق).
• الكثير من المستهلكين يعيدون العرض.
• الحالة تصبح معقدة (حمولة، أوضاع متعددة، تأثيرات جانبية).
Zustand مناسب للدرج لأنه:
• لا يحتاج إلى مزود متعدد الطبقات.
• اختيار الحالة دقيق (يقلل إعادة العرض).
• منطق معزول (أسهل في الصيانة).
ليس الأمر "من هو الأفضل"، بل "من هو الأكثر راحة للتشغيل اليومي".
إيماءات الهاتف المحمول وتجربة المستخدم الطبيعية: لماذا الدرج يبدو طبيعيًا
هناك سبب يجعل الدرج مريحًا في التجارة الإلكترونية على الهاتف المحمول: لأن نموذج المستخدم الذهني متشكل.
المستخدم معتاد على:
• السحب من الجوانب.
• السحب للأسفل للإغلاق.
• لوحات متراكبة (bottom sheet، panel، overlay).
الدرج يستفيد من هذه العادات. الصفحة الكاملة قد تكون جيدة أيضًا، لكن للعمليات السريعة (عرض التفاصيل، إضافة للعربة، اختيار المتغيرات)، الدرج يبدو أقرب إلى تجربة التطبيقات الأصلية.
إذا تم تحسين الدرج جيدًا (إيماءات الإغلاق، الرسوم المتحركة السلسة، التركيز المنظم)، يمكن لموقع التجارة الإلكترونية أن يبدو كتطبيق — دون إجبار المستخدم على ترك نمط الويب المألوف (URL وروابط المشاركة).
تأمل أخير
لتبسيط الأمر، الاختيار بين الدرج والصفحة الكاملة ليس مسألة واجهة مستخدم، بل نية المستخدم.
• إذا أراد المستخدم القراءة → الصفحة الكاملة.
• إذا أراد المستخدم الفعل → الدرج.
الدرج الجيد عادة:
• مدفوع بالحالة.
• آمن نوعيًا.
• متاح.
• مناسب للهاتف المحمول.
• ولا يطمع في استبدال كل شيء.
وعندما تتجمع كل هذه العناصر، يصبح الدرج أكثر من مجرد مكون واجهة مستخدم، بل هو هيكلية تفاعل.
