الإقليم ٠٤ — فعلٌ أم تزامن؟
في الإقليم ٣ كل مثال كان من نمطٍ واحد: "بياناتٌ تغيّرت (أو المكوّن ظهر) ← افعل شيئاً". لكن تسجيل الدخول لا يشبه هذا إطلاقاً: لا "بيانات تغيّرت تلقائياً" هنا — يوجد نقرة مستخدمٍ واحدة، مباشرة، وواعية. استعمال الأداة الخطأ هنا ينتج كوداً يبدو سليماً تماماً، ويعمل في أغلب الأحيان — حتى يتعطّل بصمت في أسوأ لحظة.
يبني فوق: الأثر كمزامنةٍ مع تبعيات (٣)، والحالة كذاكرةٍ تُطلق رسماً (٢). النطاق: متى تضع الكود في معالج حدث، ومتى في أثر — قرارٌ يبدو تفصيلاً أسلوبياً، وهو في الحقيقة يحسم صحّة تطبيقك. يفتح: أدوات الذاكرة الإضافية (٥)، والخطافات المخصّصة (٦).
اللغز المستفزّ
ابنِ نموذج دخول. الحدس الأول بعد الإقليم ٣: "أي فعلٍ جانبي (هنا: طلب شبكة) مكانه Effect" — فتكتب:
type Status = "idle" | "loading" | "error" | "success";
function LoginForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [submitted, setSubmitted] = useState(false); // "علمٌ" يطلب الإرسال
const [status, setStatus] = useState<Status>("idle");
function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
setSubmitted(true); // فقط اطلب لاحقاً
}
useEffect(() => {
if (!submitted) return;
setStatus("loading");
fakeLogin(email, password)
.then(() => setStatus("success"))
.catch(() => setStatus("error"));
// ⚠️ لاحظ: لا يوجد setSubmitted(false) هنا — سنعود لهذا
}, [submitted]);
return (/* ...نموذجٌ بحقلين وزر... */);
}
شغّلها. أدخل بيانات خاطئة عمداً، اضغط "دخول". تظهر status === "error"، جيد. الآن صحّح كلمة المرور، واضغط "دخول" مرّة ثانية.
لاحظ بدقّة: لا شيء يحدث. لا طلب شبكة جديد، لا تغيّر في
status، الزر ميتٌ تماماً حتى تُنعش الصفحة بالكامل.
السؤال المستفزّ: كل سطرٍ في الكود أعلاه صحيحٌ نحوياً ومنطقياً بمعزل عن غيره. أين بالضبط ينكسر، ولماذا يعمل في أول محاولة ثم يموت في الثانية؟
ليش: التشخيص — قيمةٌ لم تتغيّر لا تُطلق شيئاً
تذكّر الإقليم ٢: setState تُطلق رسماً جديداً حين تتغيّر القيمة. لكن في أول محاولة، submitted كانت false، فـsetSubmitted(true) غيّرها فعلاً (false → true) — رسمٌ جديد، الأثر يعمل، الطلب يُرسَل. لكن الأثر لم يُعِد submitted إلى false أبداً بعد انتهاء الطلب (لا في النجاح ولا في الفشل). فحين تضغط "دخول" ثانيةً، handleSubmit يستدعي setSubmitted(true) — لكن submitted already true من المحاولة الأولى! القيمة لم تتغيّر، فـReact — بحسب نفس القاعدة التي تعلّمتَها — لا يُطلق رسماً جديداً إطلاقاً، وبالتالي لا يُعاد تشغيل الأثر أبداً. الزر لا يزال يستجيب للنقر (معالج الحدث نُفِّذ فعلاً) لكن أثر الإرسال الفعلي معطّلٌ صامتاً.
يمكنك إصلاح هذه الحالة بعينها (أضف .finally(() => setSubmitted(false)))، لكن هذا يعالج العرض لا المرض. المشكلة الجذرية أعمق: ربطتَ فعلاً واحداً محدّداً (اضغط، أرسل) بآلية مصمَّمة لشيءٍ مختلف تماماً (زامِن مع حالةٍ يمكن أن تتغيّر لعدّة أسباب). هذا يُنتج طبقة وسيطة (submitted) لا تحمل أي معلومةٍ حقيقية غير "طُلِب إرسال" — بينما الحدث نفسه (onSubmit) يعرف هذا بالفعل، فوراً، بلا وسيط.
ليش: القاعدة المشتقّة — من الذي "يعرف" سبب التنفيذ؟
اسأل نفسك عن أي كودٍ جانبي: من يعرف بالضبط لماذا يجب أن يُنفَّذ الآن؟
- معالج حدث (event handler): يُنفَّذ فقط بسبب تفاعلٍ محدّدٍ ومباشر وقع الآن (نقرة، إرسال، ضغطة مفتاح). كل استدعاءٍ له مستقلٌّ عن سابقه — ليست هناك "قيمة سابقة" يُقارَن بها؛ الدالّة تُستدعى، تُنفَّذ، تنتهي. السببية هنا مباشرة وواضحة 100%: "نُفِّذ لأن المستخدم فعل كذا الآن بالضبط".
- أثرٌ (
useEffect): يُنفَّذ لأن حالة النظام (تبعياته) اختلفت عن المرّة الماضية — بغض النظر عن سبب ذلك الاختلاف. قد يكون سببه نقرة مستخدم، أو استجابة شبكة سابقة، أو مكوّنٌ أبٌ مرّر قيمة جديدة، أو أي شيء. الأثر لا يعرف ولا يحتاج أن يعرف أيّ سببٍ بالتحديد — همّه فقط: "الحالة الآن مختلفة عن آخر مرّة، زامِن معها".
القاعدة العملية: فعلٌ محدّدٌ ومباشر من المستخدم (وأنت تعرف بالضبط ما هو) → معالج حدث. مزامنةٌ مستمرّة مع حالةٍ قد تتغيّر لأسبابٍ متعدّدة لا تهمّك جميعاً → أثر. تسجيل الدخول ينتمي للفئة الأولى بوضوح: لا "حالة تتغيّر من الخارج" هنا، فقط ضغطة زر.
كيف: الحلّ — الفعل مباشرةً في معالج الحدث
function LoginForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [status, setStatus] = useState<Status>("idle");
const [error, setError] = useState("");
async function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
setStatus("loading");
setError("");
try {
await fakeLogin(email, password);
setStatus("success");
} catch (err) {
setStatus("error");
setError(err instanceof Error ? err.message : "فشل الدخول");
}
}
return (
<form onSubmit={handleSubmit} noValidate>
<input
type="email"
value={email} // مضبوطٌ: القيمة من الحالة
onChange={(e) => setEmail(e.target.value)} // كل تغيير يحدّث الحالة
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
{status === "error" && <p role="alert">{error}</p>}
<button disabled={status === "loading"}>
{status === "loading" ? "...جارٍ الدخول" : "دخول"}
</button>
</form>
);
}
لا حاجة لأي useEffect هنا إطلاقاً. كل نقرة "إرسال" تُنفّذ handleSubmit من جديد بالكامل، بلا قيد "هل تغيّرت قيمةٌ عن سابقتها" — لأن الدوال العادية (خلافاً للحالة) ليس لها "قيمةٌ سابقة" يُقارَن بها؛ هي فقط تُستدعى. البقّة اختفت من جذرها، لا بترقيعها.
النماذج المضبوطة (controlled inputs) — نفس درس الإقليم ٠
لاحظ value={email} مع onChange: قيمة الحقل مصدرها الحالة، لا DOM نفسه. هذا يمنع بالضبط نفس شذوذ "نسختين من الحقيقة" (الإقليم ٠): لولا هذا، كانت قيمة الـ<input> الفعلية تعيش داخل DOM (خارج نظرك)، بينما email تعيش في حالتك — نسختان يمكن أن تنحرفا. بربط القيمة بالحالة، يصير الحقل مجرّد انعكاسٍ مرئي لحالتك، مطابقاً لمبدأ UI = f(state) بحذافيره على أصغر عنصرٍ تفاعلي ممكن.
(البديل — حقل غير مضبوط، تقرأ قيمته فقط لحظة الحاجة بدل تتبّعها بحالة — موجودٌ ومفيدٌ أحياناً، ويحتاج أداةً لم نتعلّمها بعد لقراءة DOM مباشرة. بذرة للإقليم ٥.)
اللغز/البناء من الصفر
اللغز أ — أعِد بناء LoginForm من الصفر بالطريقة الصحيحة أعلاه (بلا أي useEffect). أضف: تعطيل الزر أثناء loading، ورسالة خطأ تظهر وتختفي بشكلٍ صحيح بين المحاولات. معيار النجاح: أرسل بيانات خاطئة، شاهد الخطأ، صحّح البيانات، أرسل مجدداً بلا إعادة تحميل الصفحة — يجب أن يعمل الزر في كل مرة بلا استثناء.
اللغز ب — القرار المعاكس: مربّع بحثٍ حيّ. ابنِ SearchBox: حقل نصّي، وكل تغيير في محتواه يجب أن يجلب قائمة اقتراحات مطابقة من دالّة محاكاة (fakeSearch(query): Promise<string[]>) ويعرضها. القيد الحاكم لهذا اللغز تحديداً: يجب أن تستخدم useEffect هنا (لا معالج حدث خالص) — لأن جلب الاقتراحات يجب أن يتزامن مع قيمة query الحالية أياً كان سبب تغيّرها (كتابة، مسح، تراجع). طبّق أيضاً حماية سباق التسابق من الإقليم ٣ (المكتوبة سريعاً قد تصل نتائجها بترتيبٍ معكوس). اكتب بجملة واحدة: لماذا هذا المكوّن ينتمي لفئة "أثر" بينما LoginForm ينتمي لفئة "معالج حدث" — بالإشارة لمعيار "من يعرف سبب التنفيذ" أعلاه، لا وصفاً عاماً.
اللغز ج — أفسِد اللغز أ عمداً بنفس بقّة النبذة، ثم شخّصها كتابةً. عد لـLoginForm الصحيحة من اللغز أ، وحوّلها عمداً لنمط "العلم + أثر" كما في اللغز الاستفزازي، لكن هذه المرّة بلا إعادة تعيين العلم بعد الفشل فقط (اتركها تُصلَح بعد النجاح). أعد إنتاج بقّة "الزر يموت بعد فشلة واحدة"، وصِف بجملتين دقيقتين: أي سطرٍ بالضبط هو سبب البقّة، وأي قاعدة من الإقليم ٢ تفسّرها.
الخلاصة — أين تتّصل هذه العقدة بالشجرة
- ما بنيتَه: معياراً حاسماً لاختيار الأداة الصحيحة — سببية مباشرة ومعروفة = معالج حدث؛ مزامنةٌ مستمرّة مع حالةٍ متعدّدة الأسباب = أثر. بنيتَ كلا النموذجين (
LoginFormبمعالج حدث،SearchBoxبأثر) لتشعر بالفرق عملياً لا نظرياً فقط. - العقدة الجديدة: *الفعل المباشر مقابل التزامن المستمر* — هذا التمييز سيحدّد قرارات تصميم في كل مكوّنٍ تكتبه بعد هذا الإقليم.
- بابٌ مفتوح: كل "ذاكرة" استعملناها حتى الآن (
useState) تُطلق رسماً جديداً بالضرورة عند كل تغيير. لكن ماذا لو أردتَ شيئاً "يتذكّر" — مرجع DOM، عدّاد محاولاتٍ داخلي، قيمة مؤقّت — بلا أن يُسبّب أي رسمٍ جديد إطلاقاً؟ الحالة الحالية لا تكفي لهذا. → الإقليم ٥.
الوصلة بمشروعك
النمط الصحيح الذي بنيتَه في اللغز أ هو حرفياً شكل useLoginForm.ts في مشروعكم: handleSubmit دالّةٌ async تُستدعى مباشرة من onSubmit، بلا أي أثرٍ وسيط، بحالتي isLoading/serverError تُحدَّثان مباشرة داخل المعالج — لا "علمٍ" يُراقَب من بعيد. وفي المقابل، AuthContext.tsx يستعمل أثراً (useEffect بتبعية []) لاسترجاع الجلسة عند بدء التطبيق — لأن ذلك يزامن حالة المصادقة مع شيءٍ خارجي (كوكي الجلسة) عند الظهور، لا يستجيب لنقرة مستخدمٍ محدّدة. المكوّنان يجسّدان الفئتين اللتين بنيتَهما هنا تماماً، كلٌّ بالأداة الصحيحة لسببه.
البذرة التالية: عندنا الآن أداتان: حالةٌ تُطلق رسماً، وأثرٌ يُزامن بعد الرسم. لكن ليست كل "ذاكرة" يحتاجها المكوّن يجب أن تُطلق رسماً جديداً عند كل تغيير — أحياناً تريد شيئاً يبقى، بصمت، بلا أي إعلانٍ للعالم.