الإقليم ١١ — تفتيش ما قبل العرض
يبني فوق: بقّتي الإقليم ٢ (ترتيب الهوكس) والإقليم ٣ (مصفوفة تبعياتٍ ناقصة) — كنتَ شخّصتهما بنفسك يدوياً. النطاق: كيف تكتشف آلةٌ هاتين البقّتين بالذات، فوراً، قبل أن تصل للمتصفّح. يفتح: الكابستون (١٢).
اللغز المستفزّ
عد لبقّتين بنيتَهما بيدك سابقاً، وكلاهما صحيحتان نحوياً 100%:
// من الإقليم ٢: هوكٌ مشروط
function Weird({ threshold }: { threshold: number }) {
const [a, setA] = useState(1);
if (a > threshold) {
const [b, setB] = useState(100); // ⚠️ نداءٌ شرطي
}
const [c, setC] = useState(3);
return null;
}
// من الإقليم ٣: مصفوفة تبعياتٍ ناقصة
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<UserData | null>(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, []); // ⚠️ userId مفقودة من التبعيات
return null;
}
شغّل tsc --noEmit على هذين الملفّين (فحص الأنواع فقط، بلا بناء). صفر أخطاء. كلاهما يمرّان بنظافة تامة. لماذا؟ لأن كل شيءٍ هنا نوعياً صحيح 100%: a رقمٌ، b رقمٌ، userId نصٌّ يُستخدم كنص بشكل صحيح تماماً في مكانه. لا يوجد أي خطأ نوع هنا إطلاقاً — الخطأ في معنى الكود بالنسبة لقواعد React الخاصة (ترتيب استدعاء الهوكس، دلالة مصفوفة التبعيات)، وفحص الأنواع لا يعرف عن React شيئاً أصلاً — يتحقّق فقط أن الأشكال متطابقة.
السؤال المستفزّ: أنت اكتشفتَ هاتين البقّتين بالتجربة اليدوية المؤلمة (راقب، خمّن، جرّب). كيف تجعل آلة تخبرك بهما فوراً، وأنت تكتب، بلا أن تشغّل شيئاً أصلاً؟
ليش: فحص الأنواع وفحص الأنماط عملان مختلفان تماماً
tsc يجيب سؤالاً واحداً: *هل الأشكال (types) متوافقة في كل مكان؟* لا يعرف شيئاً عن "هذه دالّة React"، ولا عن "هذا الاستدعاء الثاني لـuseState يجب أن يقع في نفس الموضع في كل رسم" — هذه قواعد دلالية خاصة بإطار عمل معيّن، لا قواعد نوعٍ عامّة. تحتاج أداةً مختلفة تماماً، تفهم أنماط الاستخدام لا الأشكال: linter (فاحص أنماط) — يقرأ الكود كشجرة نحوية (AST) ويبحث عن أنماطٍ معروفةٍ بأنها خطأ أو خطرة، بصرف النظر عن صحّة أنواعها.
ESLint هو هذا الفاحص. لوحده لا يعرف شيئاً عن React — يحتاج إضافات (plugins) تُعلّمه أنماطاً بعينها. eslint-plugin-react-hooks تحديداً تحتوي قاعدتين ستتعرّف عليهما فوراً:
rules-of-hooks— تتحقّق آلياً أن كل استدعاء هوكٍ يقع في الموضع الأعلى من دالّة المكوّن (لا داخلif/حلقة/دالّة متداخلة). هذا بالضبط ما كسرتَه عمداً في الإقليم ٢.exhaustive-deps— تقرأ جسم كلuseEffect/useMemo/useCallback، تجمع كل قيمةٍ من خارج الدالّة قرأتَها بداخله، وتقارنها بمصفوفة التبعيات المكتوبة — أي قيمةٍ مفقودة تُصبح تحذيراً صريحاً. هذا بالضبط ما كسرتَه عمداً في الإقليم ٣.
كيف: ابنِ eslint.config.js من ملفٍّ فارغ
الإصدارات الحديثة من ESLint تستعمل flat config: ملفٌّ واحدٌ يُصدِّر مصفوفة من كائنات إعداد، تُقرأ من الأعلى للأسفل بترتيبٍ صريح — بديلاً عن نظام .eslintrc القديم القائم على "توريث" إعداداتٍ متعدّدة (extends) يصعب تتبّع "من أين جاءت هذه القاعدة بالضبط" فيه. كل عنصرٍ في المصفوفة يحدّد لأي ملفّاتٍ ينطبق (files) وبأي قواعد (rules) أو إضافات (plugins). ابنِه قطعةً قطعة:
الخطوة ١ — الأساس، بلا فهم TypeScript بعد. ESLint افتراضياً يفهم JS فقط — لا يعرف قراءة interface/<T>/تعليقات الأنواع، ستفشل عليها كأخطاء نحوٍ صريحة. يحتاج مُفسِّراً (parser) بديلاً:
// eslint.config.js
import tseslint from "typescript-eslint";
export default tseslint.config(
tseslint.configs.recommended, // يضيف المُفسِّر + قواعد TS الأساسية
);
الخطوة ٢ — أضف قواعد React hooks.
import tseslint from "typescript-eslint";
import reactHooks from "eslint-plugin-react-hooks";
export default tseslint.config(
tseslint.configs.recommended,
{
files: ["**/*.{ts,tsx}"],
plugins: { "react-hooks": reactHooks },
rules: {
...reactHooks.configs.recommended.rules, // rules-of-hooks + exhaustive-deps
},
},
);
الخطوة ٣ — تعارضٌ محتمل مع التنسيق. ESLint يملك أيضاً قواعد تنسيقية (مسافات، فواصل، أقواس) — لكن هذه مهمّة أداةٍ مختلفة تماماً سترى الآن لماذا.
اللغز/البناء من الصفر — الجزء الأول
اللغز أ. ابنِ eslint.config.js كما في الخطوتين أعلاه، ثبّت الحزم اللازمة (eslint, typescript-eslint, eslint-plugin-react-hooks)، وشغّله على الملفّين المكسورين في اللغز الاستفزازي. معيار النجاح: رسالتا خطأٍ واضحتان، كلٌّ تشير لاسم القاعدة (react-hooks/rules-of-hooks و react-hooks/exhaustive-deps) وموضع السطر بالضبط — بلا تشغيل التطبيق ولا النقر يدوياً لإعادة إنتاج البقّة كما فعلتَ سابقاً.
ليش: التنسيق مشكلةٌ مختلفة الطبيعة، تحتاج أداةً مختلفة
قاعدة "استدعِ الهوكس بالترتيب الصحيح" تحتاج حكماً (هل هذا النمط خطأ؟). قاعدة "استعمل علامات اقتباسٍ مزدوجة لا مفردة" لا تحتاج حكماً إطلاقاً — إجابةٌ حتميّة واحدة صحيحة، تُطبَّق آلياً على كل حرف. خلط النوعين في أداةٍ واحدة يُنتج مشكلتين: (١) قوائم قواعد تنسيقٍ طويلة تتنازع فيما بينها لو فعّلتَ أداتين تنسيقيّتين معاً، و(٢) وقتٌ ضائع في مراجعات الكود على نقاشات تنسيقية بحتة كان يمكن حسمها آلياً منذ البداية.
Prettier أداة تنسيقٍ منفصلة تماماً، حتميّة، بلا إعداداتٍ يُختلَف عليها كثيراً. لكن لو فعّلتَ قواعد ESLint التنسيقية أيضاً بجانبه، قد يتصادمان — ESLint يطلب صيغةً، Prettier يفرض أخرى، وتجد نفسك تُصلح نفس السطر مرّتين باتجاهين متعاكسين. الحلّ: eslint-config-prettier — إعدادٌ يُطفئ فقط كل قاعدة ESLint تنسيقية قد تتعارض مع Prettier، تاركاً ESLint حصرياً لقواعد المنطق/الأنماط (كـreact-hooks)، وPrettier المرجع الوحيد للتنسيق:
import tseslint from "typescript-eslint";
import reactHooks from "eslint-plugin-react-hooks";
import prettier from "eslint-config-prettier";
export default tseslint.config(
tseslint.configs.recommended,
{
files: ["**/*.{ts,tsx}"],
plugins: { "react-hooks": reactHooks },
rules: { ...reactHooks.configs.recommended.rules },
},
prettier, // ⚠️ يجب أن يكون العنصر الأخير — يطفئ ما قبله تحديداً
);
ترتيبه في المصفوفة إلزامي: كل عنصرٍ لاحقٍ في flat config يُجاوِز ما قبله لنفس القاعدة. لو وضعتَ prettier أولاً، أي قاعدة تنسيقية تُفعَّل بعده (من tseslint.configs.recommended مثلاً) ستُطفئ إطفاءه. آخر عنصرٍ يفوز دائماً.
اللغز/البناء من الصفر — الجزء الثاني
اللغز ب — أنتِج التعارض عمداً، ثم أصلحه. فعّل قاعدة ESLint تنسيقية صريحة (quotes: ["error", "double"]) في ملفّك، واضبط .prettierrc على { "singleQuote": true } (متعاكسة تماماً). شغّل ESLint على ملفٍّ بعلامات اقتباسٍ مفردة — يشتكي. شغّل Prettier على نفس الملفّ — يُصلحه لعلامات مفردة (متوافقاً مع إعداده). شغّل ESLint مرّة أخرى — يشتكي مجدداً! أنت الآن بين أداتين تتنازعان بلا توقّف. أضف eslint-config-prettier كعنصرٍ أخير في المصفوفة، وكرّر: يجب أن يتوقّف ESLint عن الاهتمام بعلامات الاقتباس نهائياً، تاركاً القرار لـPrettier وحده.
اللغز ج — بقّةٌ يمسكها tsc، لا ESLint (توازنٌ ضروري). عد لـwizardReducer الذي بنيتَه في الإقليم ٥. احذف return من إحدى حالات switch عمداً (اترك التعليق فقط، بلا return ولا break) بحيث يقع التنفيذ في الحالة التالية سهواً (fallthrough). شغّل tsc (لا ESLint) على الملفّ — يجب أن يمسكها فوراً بفضل noFallthroughCasesInSwitch في tsconfig.json (علمٌ منفصلٌ تماماً عن حزمة strict، يتحقّق من انضباط بنية switch تحديداً، لا من صحّة الأنواع). اكتب جملةً: لماذا هذه البقّة بالذات يُمسِكها فاحص الأنواع بينما بقّتا اللغز الاستفزازي لا يُمسِكهما إطلاقاً؟ هذا يمنع استنتاجاً خاطئاً شائعاً: "ESLint دائماً أقوى من tsc" — كلٌّ يمسك فئةً مختلفة من الأخطاء، ولا يُغني أحدهما عن الآخر.
الخلاصة — أين تتّصل هذه العقدة بالشجرة
- ما بنيتَه: فهماً دقيقاً للفرق بين فحص الأنواع (
tsc— شكلٌ) وفحص الأنماط (ESLint— دلالةٌ خاصّة بـReact) والتنسيق (Prettier— قرارٌ حتميٌّ بلا نقاش)؛eslint.config.jsمبنياً قطعةً قطعة تفهم كل جزءٍ فيه دوره؛ وبقّتين حقيقيّتين بنيتَهما بيدك في أقاليم سابقة، الآن تُكتشفان آلياً. - العقدة الجديدة: *طبقة تفتيشٍ آلية، مكمّلة لفحص الأنواع لا بديلة عنه.*
- بابٌ مفتوح: لا شيء جديد ميكانيكياً بعد هذا — الإقليم الأخير يجمع كل الأقاليم الثلاثة عشر في مسارٍ واحد متّصل، ويغلق كل بابٍ تُرِك مفتوحاً.
الوصلة بمشروعك
سكربت lint في package.json عندكم موجودٌ فعلاً (eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0) — لكن eslint نفسه غير مُدرَجٍ في devDependencies أصلاً. هذه فجوةٌ حقيقية بنيتَ للتو كل الأدوات اللازمة لسدّها بيدك: eslint.config.js من الصفر، مطابقاً لـtsconfig.json الصارم الموجود عندكم بالفعل (strict, noUnusedLocals, noUnusedParameters, noFallthroughCasesInSwitch)، وموجَّهاً خصيصاً لالتقاط أنماط React الخاطئة التي بنيتَها بيدك في الإقليمين ٢ و٣.
البذرة التالية: ثلاثة عشر إقليماً، كل واحدٍ بابٌ فتحته بيدك. الإقليم الأخير لا يفتح باباً جديداً — يمشي في تطبيقٍ واحدٍ صغير، ويريك كل الأبواب السابقة تُستعمَل معاً في آنٍ واحد.