ما علّمني إياه بناء OTP Input عن المكونات المتحكَّم بها
التقدم التلقائي واللصق والـ backspace تبدو بسيطة حتى تبنيها — إليك دروس الـ controlled input التي ترافقني في كل نموذج أشحنه.
الـ OTP input مجرد ست خانات صغيرة. كم يمكن أن يكون صعبًا؟ صعب بما يكفي لأنشر مكتبة كاملة له. الأخطاء التي تواجهها وأنت تبنيه هي نفسها التي تطارد كل نموذج جاد بصمت: إدارة التركيز (focus)، والتعامل مع اللصق، وإبقاء الـ state والـ DOM متطابقين. هذا ما بقي معي.
الدرس 1: مصدر حقيقة واحد، والخانات مشتقة منه
النهج الساذج يعطي كل خانة useState خاصة بها. هذه ست حالات يجب مزامنتها، وكابوس عند اللصق. احتفظ بالكود كله كنص واحد (string) واشتق كل خانة منه:
const [code, setCode] = useState("");
const digits = Array.from({ length: 6 }, (_, i) => code[i] ?? "");الآن هناك شيء واحد فقط تحدّثه وتتحقق منه وترسله. الخانات مجرد عرض للنص، وليست ست حقائق مستقلة. هذه هي عقلية المكوّن المتحكَّم به (controlled component) التي تتسع لأي حقل معقد.
الدرس 2: اللصق هو الميزة الحقيقية، وليس حالة هامشية
يلصق المستخدمون الكود من رسالة SMS أكثر بكثير مما يكتبونه. إذا عالجت onChange لكل خانة فقط، فسيضع اللصق الأرقام الستة كلها في الخانة الأولى. اعترضه:
function onPaste(e: React.ClipboardEvent) {
e.preventDefault();
const pasted = e.clipboardData.getData("text").replace(/\D/g, "").slice(0, 6);
setCode(pasted);
// move focus to the last filled box
inputs.current[Math.min(pasted.length, 5)]?.focus();
}أزل ما ليس رقمًا، وقصّ الطول، واضبط الـ state مرة واحدة، ثم انقل التركيز. معاملة اللصق كمسار أساسي هي ما يجعل المكوّن يبدو أصيلًا بدل أن يكون مزعجًا.
الدرس 3: الـ backspace يحتاج إلى قراءة الـ DOM، لا الـ state فقط
التقدم التلقائي سهل. الخطأ الدقيق هو الـ backspace: عندما تكون الخانة فارغة أصلًا، يتوقع المستخدم أن يمسح backspace الخانة السابقة ويقفز إليها. لا يمكنك معرفة ذلك من React state وحده — تحتاج إلى حدث المفتاح:
function onKeyDown(e: React.KeyboardEvent, i: number) {
if (e.key === "Backspace" && !digits[i] && i > 0) {
e.preventDefault();
setCode((c) => c.slice(0, i - 1));
inputs.current[i - 1]?.focus();
}
}التركيز الأمري (imperative) عبر مصفوفة من الـ ref هو الأداة الصحيحة هنا. الـ state المتحكَّم به يملك *القيمة*، والـ refs تملك *التركيز*. خلط هاتين المسؤوليتين هو ما يفسد معظم الـ inputs المخصصة.
الدرس 4: استدعِ onComplete من effect، لا من handler
من المغري استدعاء onComplete داخل معالج التغيير عندما يصل الطول إلى ستة. لكن اللصق وإعادة الضبط البرمجية يتجاوزان هذا المسار. اشتقّه بدلًا من ذلك:
useEffect(() => {
if (code.length === 6) onComplete?.(code);
}, [code, onComplete]);تفاعل مع القيمة، لا مع الحدث. الآن كل مسار ينتج كودًا كاملًا يُطلق الاكتمال — مرة واحدة بالضبط — دون أن تربط كل مسار يدويًا.
الخلاصة
احتفظ بمصدر حقيقة واحد واشتق الواجهة منه. دع الـ state المتحكَّم به يملك القيم والـ refs تملك التركيز. تفاعل مع الشروط المشتقة عبر effects بدل محاولة تغطية كل حدث. هذه القواعد الأربع حوّلت أداة OTP معقدة إلى شيء موثوق — وهي تجعل كل نموذج checkout وتسجيل دخول أبنيه يتصرف بالطريقة نفسها.
بقلم فادي إيهاب عامر
راسلني على واتسابتابع القراءة
التصميم بالثيمات دون خطوة بناء: كيف يستخدم NYX الدالة color-mix()
بنيت NYX، إطار مكونات بلا أي اعتماديات، بحيث يمكن إعادة ضبط ثيم كامل من حفنة custom properties — بلا Sass ولا إعادة تجميع، مباشرة في المتصفح.
بناء PWA للقرآن الكريم يعمل بدون إنترنت
بناء تطبيق قرآن يعمل offline أولًا تطلب Service Worker يصمد بلا اتصال، وحساب مواقيت الصلاة على الجهاز لأكثر من 30 دولة، وخطأين في الـ service worker علّماني التواضع.