الملاحق — ما يبقى بعد أن تنسى التفاصيل
الشجرة كاملةً بكل وصلاتها، ثلاثة عشر نموذجاً ذهنياً، ومرجعٌ سريع للاستخدام بعد إتمام المنهج.
ملحق: شجرة المهارات الكاملة
الخريطة التي كانت مكشوفةً جزئياً في README.md، الآن مكتملةً بكل الوصلات مشروحة. كل سهمٍ هنا يعني "لا يُفهَم الابن حقّاً بلا الأب".
نقاط الالتقاء الثلاث (حيث تتشابك خيوطٌ مستقلّة)
الالتقاء الأول — الإقليم ٤ يتفرّع، الإقليم ٧ يجمع: الإقليمان ٥ و٦ مستقلّان ظاهرياً (أدواتٌ إضافية مقابل تركيب المنطق)، لكنهما يلتقيان فعلياً في خطاف useAsync (الإقليم ٦) الذي يستعمل useCallback (الإقليم ٥) ليُثبّت هويّة run — بلا هذا الثبات، أي استهلاكٍ لـuseAsync داخل أثرٍ (الإقليم ٣) يدخل حلقةً لا نهائية. الإقليم ٧ (Context) لا يحتاج أياً منهما مباشرة، لكنه يرث كل دروس المزامنة والذاكرة التي بُنيت قبله.
الالتقاء الثاني — الإقليم ٧ يتفرّع لثلاثة اتجاهاتٍ مستقلّة تماماً: خريطة المسرح (٨، تنظيمٌ)، ورشة الأزياء (٩، مكوّناتٌ موثوقة)، والجولة والحارس (١٠، تنقّل) — الثلاثة لا يعتمد أحدها على الآخر مباشرة، لكن الإقليم ١٠ يستهلك كلاً من ٧ (Context لبثّ حالة التوجيه) و٩ (asChild لجعل روابط تبدو كأزرار) معاً في نفس اللغز — أول نقطةٍ في المنهج يتقاطع فيها ثلاثة أقاليم مستقلّة في لغزٍ واحد.
الالتقاء الثالث — الإقليم ١١ يغلق الدائرة على الإقليمين ٢ و٣ تحديداً: كل قاعدةٍ اشتُقّت يدوياً في الإقليمين ٢ (ترتيب الهوكس) و٣ (مصفوفة التبعيات) تُكتشَف الآن آلياً — لا كمعرفةٍ جديدة، بل كأداةٍ تُغني عن التكرار اليدوي لنفس عملية التشخيص التي أتقنتَها بيدك.
الثنائيّات الناظمة (تتكرّر بأشكالٍ مختلفة عبر المنهج كله)
- صف "ماذا" لا "كيف تُحدّث": الإقليم ٠ (المطابِق) → الإقليم ١ (المكوّن كدالّةٍ من بيانات) → الإقليم ٤ (فعلٌ محدَّدٌ مقابل تزامنٍ وصفي).
- الموضع لا الاسم: خانة الفيبر (٢) → مصفوفة التبعيات كمقارنة عنصر بعنصر (٣) → خانات
useMemo/useReducer(٥) → خانات الخطاف المخصّص تُستعار من فيبر المستدعي (٦). - استخرج المتكرّر، وثّق مصدره الوحيد: بطاقة المسار (١) → منطق النموذج (٦) → تركيبة CSS (٩) → إطار الصفحة (١٠،
Outlet). - الثمن معروفٌ ومُدار، لا مُخفى: الفانوت في Context (٧)، تكلفة المقارنة في
useMemo(٥)، التكلفة التنظيمية لـFSD (٨) — كل أداةٍ قويّة لها ثمنٌ تعرفه بدقّة بدل أن تتجاهله.
ملحق: النماذج الذهنية
ثلاثة عشر نموذجاً، كلٌّ يختزل إقليماً في جملةٍ تشتقّ منها الباقي. هذه ما يبقى بعد أن تنسى التفاصيل.
٠. صف "ماذا"، ودَع مطابِقاً يقارن بدل أن يبني من جديد
الوصف بياناتٌ محضة (شجرة نوع/خصائص/أبناء)؛ التحديث اليدوي للـDOM يُنتج نسختين من الحقيقة تتباعدان. المطابِق يقارن القديم بالجديد ويُطبّق الفرق الأدنى فقط. القوائم تحتاج هويّةً صريحة (key) لا موضعاً، وإلا اختلطت الحالة بين العناصر عند إعادة الترتيب. (٠)
١. المكوّن دالّةٌ من props إلى وصف؛ children سيناريوٌ خاص
تكرار الشكل يُستخرَج لدالّةٍ واحدة تأخذ بيانات، كأي دالّةٍ عادية. props تتدفّق لأسفل فقط — تحويرها مباشرةً يُعيد نفس تباعد الإقليم ٠. children ليست سحراً، فقط prop يملؤه محتوى ما بين وسمي الفتح والإغلاق تلقائياً. (١)
٢. الذاكرة موضعيةٌ في سجلٍّ خارج الدالّة، لا داخلها
الدالّة تُستدعى من الصفر كل رسم؛ الذاكرة تعيش في فيبر — سجلٌّ دائم لكل نسخة مكوّن. useState خانةٌ برقم ترتيب النداء، لا بالاسم. قواعد الهوكس (ترتيبٌ وعددٌ ثابتان، فقط أثناء رسمٍ فعلي) نتيجةٌ مباشرة لهذه الآلية، لا اصطلاحٌ تعسّفي. (٢، أهمّ إقليمٍ في المنهج)
٣. الأثر يُزامن مع تبعيات، لا يتبع "دورة حياة"
الرسم يجب أن يبقى نقياً؛ أي لمسٍ للعالم الخارجي ينتقل لـuseEffect، يعمل بعد استقرار الرسم. مصفوفة التبعيات عقدُ صحّة لا تحسينٌ اختياري. كل تشغيلٍ له تنظيفه الخاص، بإغلاقٍ مستقل — وهنا تُحلّ سباقات التسابق. StrictMode يشغّل مرّتين عمداً ليختبر اكتمال هذا التنظيف. (٣)
٤. من يعرف سبب التنفيذ يحدّد الأداة
سببيّةٌ مباشرة ومعروفة (نقرة) → معالج حدث، يُستدعى فقط حين يقع الفعل، بلا مقارنةٍ بقيمةٍ سابقة. مزامنةٌ مع حالةٍ متعدّدة الأسباب → أثر. خلطهما (علمٌ يُراقَب بأثر بدل فعلٍ مباشر) يُنتج بقّاتٍ تبدو صحيحة حتى تنكسر صامتة. (٤)
٥. أدواتٌ متخصّصة، أساسٌ واحد (نفس سجلّ الفيبر)
useRef: خانةٌ بكائنٍ ثابتٍ يُحوَّر في مكانه، بلا طلب رسم. useMemo/ useCallback: خانةٌ بقيمةٍ وتبعياتها، بنفس منطق مقارنة الإقليم ٣، لثبات الهويّة المرجعية. useReducer: نفس useState، بانضباط انتقالٍ مركزيٍّ عبر دالّةٍ نقيّة بدل setters مستقلّة. (٥)
٦. الخطاف المخصّص يستعير خانات المستدعي، لا يملك سجلّه الخاص
دالّةٌ عادية تستدعي هوكساً داخلها، فتخضع لنفس قواعد الترتيب عبر حدود الدوال. تُعيد استخدام السلوك كما يُعيد المكوّن استخدام الشكل. اسم use إشارةٌ لأدواتٍ لاحقة (الإقليم ١١) أن هذا يستدعي هوكساً بداخله. (٦)
٧. مكبّر الصوت العام: بثٌّ للجميع، لا اشتراكٌ انتقائي
Context يحلّ تمرير props عبر مستوياتٍ لا تستخدمها. الثمن: أي تغييرٍ في القيمة يُعيد رسم كل مستهلكٍ، بصرف النظر عمّا يهمّه فعلاً — لأن التخزين كتلةٌ واحدة، لا حقولاً منفصلة الاشتراك. الحلّ: قنواتٌ منفصلة بمعدّل التغيّر، لا كتلةٌ واحدة تخلط النادر بالمتكرّر. (٧)
٨. نظّم حسب "من يملك"، لا "ما شكله"
تجميعٌ حسب الميزة (لا نوع الملف) يحلّ تصادم الفرق بنيوياً. حدٌّ بين "شكل" (يستقبل ويعرض) و"منطق" (خطافٌ ينتج) يسمح بإعادة استخدام نفس البيانات بأشكالٍ متعدّدة. الترقية إلى shared بعد أول استخدامٍ فعلي في قسمين، لا تخميناً مسبقاً. (٨)
٩. الشكل من دالّة variants، السلوك من ref يعبر الحاجز، الهويّة تُمنَح لا تُفرَض
cva: خانة "حالاتٍ أساسية" مكتوبةٌ مرّةً تُطبَّق على كل تركيبة، تمنع بقّة النسيان بنيوياً. forwardRef: ref لا تصل لـprops تلقائياً، يحتاج توقيعاً خاصاً. asChild/Slot: امنح شكلك وسلوكك لعنصرٍ آخر بدل فرض غلافك عليه — يحلّ "أريد شيئاً يبدو كذا لكنه ليس ذاك العنصر". (٩)
١٠. الرابط حالةٌ مشتركة عبر History API، لا مجرّد نصّ في الشريط
window.location.href يُعيد تحميل الصفحة بالكامل؛ pushState يغيّر الرابط بلا ذلك، لكنه لا يُطلق popstate بنفسه — تحتاج مزامنةً يدوية صريحة. Outlet = children، لكن مصدره مطابقة رابطٍ لا تركيبٌ صريح في JSX. الحارس مسارٌ أبٌ شرطي: يمرّر (Outlet) أو يحوّل. (١٠)
١١. فحص الشكل يختلف عن فحص الأنماط الدلالية عن فحص التنسيق
tsc يتحقّق من توافق الأنواع فقط — بقّاتٌ كترتيب الهوكس ومصفوفات التبعيات الناقصة نوعياً صحيحة تماماً، فلا يمسكها. ESLint (بإضافاتٍ خاصّة بـReact) يفهم أنماط الاستخدام الدلالية. Prettier منفصلٌ تماماً — قرارٌ حتميٌّ بلا نقاش، وeslint-config-prettier يمنع تعارضهما. (١١)
١٢. كل عقدةٍ سابقة، في مسارٍ متّصلٍ واحد
لا نموذج جديد هنا — فقط إثبات أن الأسماء الثلاثة عشر أعلاه ليست معرفةً منفصلة، بل شبكةٌ واحدة تتحرّك خلالها كل نقرة، من الحدث حتى استقرار البكسل على الشاشة. (١٢)
ملحق: مرجعٌ سريع
مرجعٌ للاستخدام بعد إتمام المنهج — ليس مادّةً تعليمية. لو احتجتَ فهم *لماذا* سطرٍ هنا، ارجع لإقليمه المذكور بين قوسين.
الخطافات — إشارةٌ سريعة
| الخطاف | يُرجع | الغرض بجملة | إقليم |
|---|---|---|---|
useState(initial) | [value, setValue] | ذاكرةٌ تُطلق رسماً جديداً عند التغيّر | ٢ |
useEffect(fn, deps) | — | مزامنةٌ بعد الرسم؛ fn قد تُعيد دالّة تنظيف | ٣ |
useRef(initial) | { current } (ثابتٌ) | ذاكرةٌ بلا رسمٍ جديد؛ توصيلٌ لعقد DOM | ٥ |
useMemo(compute, deps) | قيمةٌ محفوظة | يُعيد نفس المرجع إن لم تتغيّر deps | ٥ |
useCallback(fn, deps) | نفس fn (ثابتة) | useMemo(() => fn, deps) — حالةٌ خاصّة منه | ٥ |
useReducer(reducer, init) | [state, dispatch] | useState بانضباط انتقالٍ مركزي | ٥ |
useContext(Ctx) | القيمة الحالية | التقاط قيمةٍ مبثوثة من أقرب Provider فوقه | ٧ |
قاعدة الاعتماديات (تنطبق على useEffect/useMemo/useCallback): أي قيمةٍ من خارج الدالّة تُقرَأ بداخل الكولباك يجب أن تكون في المصفوفة. [] = مرّةً واحدة. بلا مصفوفة أصلاً = كل رسم (نادراً ما تريد هذا).
قواعد الهوكس (كلاهما مُشتقّتان من آلية الفيبر الموضعية، الإقليم ٢): ١) نادِها فقط في المستوى الأعلى من مكوّنٍ أو خطافٍ مخصّص — لا شرط، لا حلقة، لا دالّة متداخلة. ٢) نادِها فقط من مكوّنات React أو خطافاتٍ تبدأ بـuse.
نمط الـvariant (على طراز CVA)
import { cva, type VariantProps } from "class-variance-authority";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md font-medium transition-colors " +
"focus-visible:ring-2 disabled:opacity-50 disabled:pointer-events-none", // الحالات الخمس، مرّةً واحدة
{
variants: {
variant: { primary: "bg-primary text-white", outline: "border" },
size: { sm: "h-8 px-3", md: "h-10 px-4", lg: "h-12 px-6" },
},
defaultVariants: { variant: "primary", size: "md" },
},
);
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
VariantProps<typeof buttonVariants> & { asChild?: boolean };
نمط forwardRef + asChild معاً
import { forwardRef } from "react";
import { Slot } from "@radix-ui/react-slot"; // أو ما يكافئه في base-ui
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ asChild, variant, size, className, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp ref={ref} className={cn(buttonVariants({ variant, size }), className)} {...props} />
);
},
);
Button.displayName = "Button";
نمط Context بخطاف حارسٍ (throw خارج الـProvider)
type ThemeContextValue = { theme: "dark" | "light"; setTheme: (t: "dark" | "light") => void };
const ThemeContext = createContext<ThemeContextValue | null>(null);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<"dark" | "light">("dark");
const value = useMemo(() => ({ theme, setTheme }), [theme]); // تثبيت الهويّة — الإقليم ٧
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}
export function useTheme(): ThemeContextValue {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme يجب أن يُستدعى داخل ThemeProvider");
return ctx;
}
قاعدة الفانوت: قنواتٌ منفصلة حسب معدّل التغيّر. لا تجمع قيمةً نادرة التغيّر (ثيم) مع قيمةٍ عالية التردّد (عدّاد حيّ) في نفس الـContext.
نمط الموجّه + الحارس
import { createBrowserRouter, RouterProvider, Outlet, Navigate, useLocation } from "react-router-dom";
function RequireAuth() {
const { isAuthenticated } = useAuth(); // من الإقليم ٧
const location = useLocation();
if (!isAuthenticated) return <Navigate to="/login" state={{ from: location }} replace />;
return <Outlet />;
}
const router = createBrowserRouter([
{
element: <MainLayout />, // Navbar/Footer مرّةً واحدة
children: [
{ path: "/", element: <HomePage /> },
{
element: <RequireAuth />, // طبقة حراسة
children: [
{ path: "/profile", element: <ProfilePage /> },
],
},
],
},
]);
eslint.config.js — هيكلٌ جاهزٌ للنسخ
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import prettier from "eslint-config-prettier";
export default tseslint.config(
{ ignores: ["dist"] },
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ["**/*.{ts,tsx}"],
plugins: { "react-hooks": reactHooks, "react-refresh": reactRefresh },
rules: {
...reactHooks.configs.recommended.rules, // rules-of-hooks + exhaustive-deps
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
},
},
prettier, // ⚠️ إلزامياً آخر عنصر
);
الحزم: npm i -D eslint typescript-eslint eslint-plugin-react-hooks eslint-plugin-react-refresh eslint-config-prettier prettier
جدول تشخيص الأعراض (نسخةٌ دائمة من لغز الإقليم ١٢)
| العرض | الإقليم | الآلية |
|---|---|---|
| زرٌّ يعمل مرّةً، يموت بعد فشلةٍ واحدة | ٤ | علمٌ لم يتغيّر = React لا يُعيد الرسم = الأثر لا يعمل |
| نتائج بحثٍ قديمة تظهر متأخّرة | ٣ | سباق تسابق — رد بطيءٌ وصل بعد رد أسرع |
| قيمة حقلٍ "تقفز" بعد إدراج عنصرٍ في قائمة | ٠ | مطابقةٌ بالموضع لا بمفتاح |
| مكوّنٌ يُعاد رسمه لتغييرٍ لا يخصّه | ٧ | فانوت Context — قناةٌ واحدة لبياناتٍ مختلفة التردّد |
| أثرٌ يطبع قيمةً قديمة دائماً | ٣ | إغلاقٌ عالق — تبعيةٌ ناقصة |
ref على مكوّنٍ مخصّص لا يتّصل | ٩ | نسي forwardRef |
| عنصرين متداخلين بدل واحد | ٩ | نسي asChild/Slot |
| صفحةٌ محميّة تظهر للحظة قبل التحويل | ١٠ | الحارس يعمل بعد الرسم الأول لا قبله |
| حالةٌ غير متّسقة بعد نقراتٍ متتابعة سريعة | ٥ | useState مستقلّة متعدّدة بدل useReducer مركزي |
tsc نظيفٌ لكن حلقة طلباتٍ لا نهائية | ٣ | fetch/أثرٌ جانبي في جسم الرسم مباشرة، لا في useEffect |
| هوكٌ يُفسد خاناتٍ أخرى أحياناً | ٢ | نداءٌ شرطي يكسر ترتيب الفيبر |
أوامر شائعة
npm run dev # خادم تطوير Vite
npm run build # tsc -b && vite build
npx eslint . --fix # فحص + إصلاحٌ آلي لما يُصلَح آلياً
npx prettier --write . # تنسيقٌ آلي كامل