اختيار هيكلة الحالة
إن هيكلة للحالة (state) بشكل صحيح هي أحد العناصر الأساسية التي يمكن أن تجعل المكون (component) سهل التعديل والتصحيح عند مواجهة الأخطاء، أو بالعكس، قد تكون مصدراً للتعقيد والمشاكل في مشروعك. لذا، إليك بعض النصائح التي يمكنك اتباعها لهيكلة الحالات (state) بشكل فعّال:
You will learn
- متى تستخدم متغير حالة واحد مقابل عدة متغيرات حالة
- ما يجب تجنبه عند تنظيم الحالة
- كيفية إصلاح المشكلات الشائعة في هيكلة الحالة
مبادئ هيلكة الحالات
عندما تقوم بكتابة مكون يحتوي على حالة، ستحتاج إلى اتخاذ قرارات بشأن عدد متغيرات الحالة التي ستستخدمها وشكل البيانات التي تحتويها. بينما من الممكن كتابة برامج صحيحة حتى مع هيكلة حالة غير مثلى، هناك بعض المبادئ التي يمكن أن تساعدك في اتخاذ قرارات أفضل:
- دمج الحالات المتشابهة اذا كنت تحدث اثنين او اكثر من الحالات في آن واحدة. فكر في دمجهم الى حالة واحدة
- تجنب تناقضات في الحالة. عندما تكون الحالة مهيكلة بطريقة يكون فيها اجزاء من الحالة متناقضة, فهذا يفتح مجال للأخطاء. حاول تجنبها
- تجنب الحالة الزائدة. اذا كنت تعالج معلومة ما عن طريق خصائص (prop) او الحالة موجودة مسبقا. فيجب عليك تفادي وضع تلك المعلومة في حالات ذلك المكون
- تجنب تكرار الحالات. عندما تكون نفس البيانات مكررة بين عدة حالات، أو ضمن كائنات متداخلة، يصبح من الصعب الحفاظ على تزامنها. تفادى التكرار قدر الإمكان.
- تجنب الحالة المتداخلة بعمق. تجنب الحالة المتداخلة بعمق. الحالة ذات الهيكلية العميقة ليست مناسبة للتحديث. يُفضّل هيكلة الحالة بطريقة مسطحة.
الهدف من هذه المبادئ هو تسهيل تحديث الحالة دون فتح مجال للأخطاء. من خلال إزالة البيانات الزائدة والمتكررة من الحالة، نضمن تماسك جميع أجزائها. هذا مماثل لما قد يفعله مهندس قاعدة المعلومات “تطبيع” لهيكلة قاعدة المعلومات لتقليل من الأخطاء المحتملة. كما قال أينشتاين, “اجعل حالتك بسيطة بقدر الإمكان—لكن لا تجعلها أبسط من ذلك.”
لنرى الآن كيف يمكن تطبيق هذه المبادئ عملياً.
جمع الحالة المتعلقة
قد تكون أحياناً غير متأكد مما إذا كنت ينبغي أن تستخدم متغير حالة واحداً أم عدة متغيرات حالة.
هل يجب عليك فعل هذا؟
const [x, setX] = useState(0);
const [y, setY] = useState(0);
او هذا؟
const [position, setPosition] = useState({ x: 0, y: 0 });
من الناحية التقنية، يمكنك استخدام أي من هاتين الطريقتين. ولكن إذا كان هناك متغيران من الحالة يتغيران دائماً معاً، فقد يكون من الجيد دمجهما في متغير حالة واحد. بذلك، لن تنسى دائماً الحفاظ على تزامنهما، كما في هذا المثال حيث يؤدي تحريك المؤشر إلى تحديث إحداثيات النقطة الحمراء:
import { useState } from 'react'; export default function MovingDot() { const [position, setPosition] = useState({ x: 0, y: 0 }); return ( <div onPointerMove={e => { setPosition({ x: e.clientX, y: e.clientY }); }} style={{ position: 'relative', width: '100vw', height: '100vh', }}> <div style={{ position: 'absolute', backgroundColor: 'red', borderRadius: '50%', transform: `translate(${position.x}px, ${position.y}px)`, left: -10, top: -10, width: 20, height: 20, }} /> </div> ) }
في حالة أخرى، قد تحتاج إلى جمع بيانات الحالة في كائن أو مصفوفة عندما لا تعرف عدد أجزاء الحالة التي ستحتاجها. على سبيل المثال، ستكون هذه الطريقة مفيدة عندما يكون لديك نموذج يتيح للمستخدم إضافة حقول مخصصة
تجنب التناقضات في حالات
هنا نموذج تقييم للفندق يحتوي على متغيري حالة isSending
و isSent
:
import { useState } from 'react'; export default function FeedbackForm() { const [text, setText] = useState(''); const [isSending, setIsSending] = useState(false); const [isSent, setIsSent] = useState(false); async function handleSubmit(e) { e.preventDefault(); setIsSending(true); await sendMessage(text); setIsSending(false); setIsSent(true); } if (isSent) { return <h1>شكرًا لتقديم الملاحظات!</h1> } return ( <form onSubmit={handleSubmit}> <p>كيف كانت إقامتك في فندق The Prancing Pony؟</p> <textarea disabled={isSending} value={text} onChange={e => setText(e.target.value)} /> <br /> <button disabled={isSending} type="submit" > ارسال </button> {isSending && <p>يتم الارسال...</p>} </form> ); } // تظاهر بارسال رسالة function sendMessage(text) { return new Promise(resolve => { setTimeout(resolve, 2000); }); }
بينما يعمل هذا الكود، فإنه يسمح لحالات “مستحيلة”. على سبيل المثال، إذا نسيت منادات setIsSent
و setIsSending
معا, قد تجد نفسك في موقف تكون فيه كل من isSending
و isSent
بقيمة true
في نفس الوقت. و كلما زاد تعقيد المكون الخاص بك كلما صعب عليك فهم ما حصل.
بما أن isSending
و isSent
يجب ان لا يكونا true
في الوقت نفسه, فمن الافضل استبدالهم ب status
متغير الحالة الذي يمكنه أخذ ثلاث حالات فقط: 'typing'
(بداية), 'sending'
, و 'sent'
:
بحيث typing يتم الكتابة sending: يتم ارسال sent تم ارسال
import { useState } from 'react'; export default function FeedbackForm() { const [text, setText] = useState(''); const [status, setStatus] = useState('typing'); async function handleSubmit(e) { e.preventDefault(); setStatus('sending'); await sendMessage(text); setStatus('sent'); } const isSending = status === 'sending'; const isSent = status === 'sent'; if (isSent) { return <h1>شكرًا لتقديم الملاحظات!</h1> } return ( <form onSubmit={handleSubmit}> <p>كيف كانت إقامتك في فندق The Prancing Pony؟</p> <textarea disabled={isSending} value={text} onChange={e => setText(e.target.value)} /> <br /> <button disabled={isSending} type="submit" > ارسال </button> {isSending && <p>يتم الارسال...</p>} </form> ); } // تظاهر بارسال رسالة function sendMessage(text) { return new Promise(resolve => { setTimeout(resolve, 2000); }); }
يمكنك تعريف ثابت من أجل تسهيل قراءة:
const isSending = status === 'sending';
const isSent = status === 'sent';
لكنهم ليسوا متغيرات حالة، لذا لا داعي للقلق بشأن فقدان التزامن بينها.
تجنب تناقضات في الحالة.
إذا كنت تستطيع حساب بعض المعلومات من خصائص المكون أو من متغيرات الحالة الموجودة أثناء عملية العرض، يجب عليك عدم وضع تلك المعلومات في حالة المكون.
فمثلا, خذ هذا النموذج. إنه يعمل, ولكن هل يمكنك العثور على أي حالة زائدة فيه؟
import { useState } from 'react'; export default function Form() { const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const [fullName, setFullName] = useState(''); function handleFirstNameChange(e) { setFirstName(e.target.value); setFullName(e.target.value + ' ' + lastName); } function handleLastNameChange(e) { setLastName(e.target.value); // هنا يتم اضافت اسم الاول و اخير معا لتشكيل الاسم كاملا setFullName(firstName + ' ' + e.target.value); } return ( <> <h2>فل نسجلك</h2> <label> الاسم الأول:{' '} <input value={firstName} onChange={handleFirstNameChange} /> </label> <label> الاسم الأخير:{' '} <input value={lastName} onChange={handleLastNameChange} /> </label> <p> تذكرتك ستصدر الى: <b>{fullName}</b> </p> </> ); }
هذا النموذج لديه ثلاث متغيرات الحالة: firstName
, lastName
, و fullName
. مع ذلك, fullName
غير ضرورية. يمكنك دائما حساب fullName
انطلاقا firstName
و lastName
اثناء العرض, اذا يمكن حذفها.
اليك الطريقة كيف يمكنك قيام بذالك:
import { useState } from 'react'; export default function Form() { const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const fullName = firstName + ' ' + lastName; function handleFirstNameChange(e) { setFirstName(e.target.value); } function handleLastNameChange(e) { setLastName(e.target.value); } return ( <> <h2>فل نسجلك</h2> <label> الاسم الأول:{' '} <input value={firstName} onChange={handleFirstNameChange} /> </label> <label> الاسم الأخير:{' '} <input value={lastName} onChange={handleLastNameChange} /> </label> <p> تذكرتك ستصدر الى: <b>{fullName}</b> </p> </> ); }
هنا, fullName
ليس متغير حالة. ولكن, يتم حسابه أثناء العرض:
const fullName = firstName + ' ' + lastName;
نتيجة لذلك، لا تحتاج معالجات التغييرات إلى القيام بشيء خاص لتحديثها. عند استدعاء setFirstName
أو setLastName
، فإنك تُحفِّز عملية إعادة العرض، ومن ثم سيتم حساب fullName
التالي من البيانات الجديدة..
Deep Dive
مثال شائع على الحالة الزائدة هو الكود التالي:
function Message({ messageColor }) {
const [color, setColor] = useState(messageColor);
هنا, color
متغير حالة تم تهيئته بالخاصية messageColor
. المشكل في هذه الطريقة اذا قدم أب المكون قيمة اخرى للخاصية messageColor
اثناء العرض (مثلا, 'red'
في مكان 'blue'
), متغير الحالة فان color
لن يتم تحديثه! تُهيَّأ الحالة فقط خلال عملية العرض الأولى.
لهذا السبب، يؤدي نسخ الخصائص إلى متغيرات الحالة إلى حدوث ارتباك. بدلاً من ذلك، يمكنك استخدام الخاصية messageColor مباشرةً، أو إذا كنت ترغب في تغيير اسمها في المكون، يمكنك استخدام ثابت
function Message({ messageColor }) {
const color = messageColor;
بهذه الطريقة، لن تفقد الخاصية التزامن مع المكون الأب.
”النسخ” خاصية الى متغير الحالة يكون منطقيًا فقط عندما تريد ان تجاهل جميع التحديثات المتعلقة بالخاصية
تقليديا, ابدأ اسم الخاصية ب initial
أو default
لتوضح أنه سيتم تجاهل القيم الجديدة.
function Message({ initialColor }) {
// متغير الحالة `color` يحتفظ ب *أول* قيمة ل `initialColor`.
// أما تغيرات جديدة للخاصية `initialColor` سيتم تجاهلها
const [color, setColor] = useState(initialColor);
تجنب تكرار
تتيح لك هذه المكونة لقائمة القائمة اختيار وجبة خفيفة واحدة من عدة خيارات:
import { useState } from 'react'; const initialItems = [ { title: 'بريتزل', id: 0 }, { title: 'أعشاب بحرية مقرمشة', id: 1 }, { title: 'شريط جرانولا', id: 2 }, ]; export default function Menu() { const [items, setItems] = useState(initialItems); const [selectedItem, setSelectedItem] = useState( items[0] ); return ( <> <h2>ماهي وجبتك الخفيفة?</h2> <ul> {items.map(item => ( <li key={item.id}> {item.title} {' '} <button onClick={() => { setSelectedItem(item); }}>اختر</button> </li> ))} </ul> <p>لقد اخترت {selectedItem.title}.</p> </> ); }
حاليا, يتم تخزين العنصر المختار ككائن في selectedItem
متغير الحالة. ولكن, هذا ليس مثاليًا **بسبب ان محتويات selectedItem
هي نفسها الكائن كواحد من محتويات قائمةitems
فهذا يعني ان المعلومة حول العنصر متكررة في مكانين
لماذا يعتبر هذا الشيء مشكلة؟
import { useState } from 'react'; const initialItems = [ { title: 'بريتزل', id: 0 }, { title: 'أعشاب بحرية مقرمشة', id: 1 }, { title: 'شريط جرانولا', id: 2 }, ]; export default function Menu() { const [items, setItems] = useState(initialItems); const [selectedItem, setSelectedItem] = useState( items[0] ); function handleItemChange(id, e) { setItems(items.map(item => { if (item.id === id) { return { ...item, title: e.target.value, }; } else { return item; } })); } return ( <> <h2>ماهي وجبتك الخفيفة?</h2> <ul> {items.map((item, index) => ( <li key={item.id}> <input value={item.title} onChange={e => { handleItemChange(item.id, e) }} /> {' '} <button onClick={() => { setSelectedItem(item); }}>اختر</button> </li> ))} </ul> <p>لقد اخترت {selectedItem.title}.</p> </> ); }
لاحظ كيف أنه إذا قمت أولاً بالنقر على “اختر” على عنصر ثم قمت بتعديله، تتحدث المدخلات ولكن التسمية في الأسفل لا تعكس التعديلات. وهذا لأن لديك حالة مكررة، ونسيت تحديث selectedItem
.
على الرغم من أنه يمكنك تحديث selectedItem
أيضًا، فإن الإصلاح الأسهل هو إزالة التكرار. في هذا المثال، بدلاً من استخدام كائن selectedItem
(الذي يخلق تكرارًا مع الكائنات داخل items)، تحتفظ بـ selectedId
في الحالة، وثم تحصل على selectedItem
من خلال البحث في مصفوفة items
عن عنصر له هذا المعرف id
:
import { useState } from 'react'; const initialItems = [ { title: 'بريتزل', id: 0 }, { title: 'أعشاب بحرية مقرمشة', id: 1 }, { title: 'شريط جرانولا', id: 2 }, ]; export default function Menu() { const [items, setItems] = useState(initialItems); const [selectedId, setSelectedId] = useState(0); const selectedItem = items.find(item => item.id === selectedId ); function handleItemChange(id, e) { setItems(items.map(item => { if (item.id === id) { return { ...item, title: e.target.value, }; } else { return item; } })); } return ( <> <h2>ماهي وجبتك الخفيفة?</h2> <ul> {items.map((item, index) => ( <li key={item.id}> <input value={item.title} onChange={e => { handleItemChange(item.id, e) }} /> {' '} <button onClick={() => { setSelectedId(item.id); }}>اختر</button> </li> ))} </ul> <p>لقد اخترت {selectedItem.title}.</p> </> ); }
الحالة كانت متكررة هكذا:
items = [{ id: 0, title: 'بريتزل'}, ...]
selectedItem = {id: 0, title: 'بريتزل'}
ثم بعد التغييرات رجعت كهذا:
items = [{ id: 0, title: 'بريتزل'}, ...]
selectedId = 0
تمت إزالة التكرار، وتحتفظ الآن بالحالة الأساسية فقط!
الآن، إذا قمت بتعديل العنصر المحدد، ستُحدَّث الرسالة أدناه على الفور. وذلك لأن setItems
يُحفِّز إعادة العرض، و items.find(...)
سيجد العنصر بعنوانه المحدث. لم تكن بحاجة إلى الاحتفاظ بـ العنصر المحدد في الحالة، لأن معرّف العنصر المحدد فقط هو الأساسي. يمكن حساب البقية أثناء العرض.
تجنب الحالة المتداخلة بعمق.
تخيل خطة سفر تتكون من كواكب وقارات ودول. قد تشعر بالميل لهيكلة حالتها باستخدام كائنات وأ Arrays متداخلة، مثلما في هذا المثال:
export const initialTravelPlan = { id: 0, title: '(Root)', childPlaces: [{ id: 1, title: 'الأرض', childPlaces: [{ id: 2, title: 'افريقيا', childPlaces: [{ id: 3, title: 'بوتسوانا', childPlaces: [] }, { id: 4, title: 'مصر', childPlaces: [] }, { id: 5, title: 'كينيا', childPlaces: [] }, { id: 6, title: 'مدغشقر', childPlaces: [] }, { id: 7, title: 'المغرب', childPlaces: [] }, { id: 8, title: 'نيجيريا', childPlaces: [] }, { id: 9, title: 'افريقيا', childPlaces: [] }] }, { id: 10, title: 'الأمريكتين', childPlaces: [{ id: 11, title: 'الأرجنتين', childPlaces: [] }, { id: 12, title: 'البرازيل', childPlaces: [] }, { id: 13, title: 'بربادوس', childPlaces: [] }, { id: 14, title: 'كندا', childPlaces: [] }, { id: 15, title: 'جامايكا', childPlaces: [] }, { id: 16, title: 'المكسيك', childPlaces: [] }, { id: 17, title: 'ترينيداد وتوباغو', childPlaces: [] }, { id: 18, title: 'فنزويلا', childPlaces: [] }] }, { id: 19, title: 'آسيا', childPlaces: [{ id: 20, title: 'الصين', childPlaces: [] }, { id: 21, title: 'الهند', childPlaces: [] }, { id: 22, title: 'سنغافورة', childPlaces: [] }, { id: 23, title: 'كوريا الجنوبية', childPlaces: [] }, { id: 24, title: 'تايلاند', childPlaces: [] }, { id: 25, title: 'فيتنام', childPlaces: [] }] }, { id: 26, title: 'أوروبا', childPlaces: [{ id: 27, title: 'كرواتيا', childPlaces: [], }, { id: 28, title: 'فرنسا', childPlaces: [], }, { id: 29, title: 'ألمانيا', childPlaces: [], }, { id: 30, title: 'إيطاليا', childPlaces: [], }, { id: 31, title: 'البرتغال', childPlaces: [], }, { id: 32, title: 'إسبانيا', childPlaces: [], }, { id: 33, title: 'تركيا', childPlaces: [], }] }, { id: 34, title: 'أوقيانوسيا', childPlaces: [{ id: 35, title: 'أستراليا', childPlaces: [], }, { id: 36, title: 'بورا بورا (بولينيزيا الفرنسية)', childPlaces: [], }, { id: 37, title: 'جزيرة القيامة (الشيلي)', childPlaces: [], }, { id: 38, title: 'فيجي', childPlaces: [], }, { id: 39, title: 'هاواي (ولايات متحدة الامريكية)', childPlaces: [], }, { id: 40, title: 'نيوزلندا', childPlaces: [], }, { id: 41, title: 'فانواتو', childPlaces: [], }] }] }, { id: 42, title: 'القمر', childPlaces: [{ id: 43, title: 'ريتا', childPlaces: [] }, { id: 44, title: 'بيكولوميني', childPlaces: [] }, { id: 45, title: 'تايكو', childPlaces: [] }] }, { id: 46, title: 'المريخ', childPlaces: [{ id: 47, title: 'مدينة الذرة', childPlaces: [] }, { id: 48, title: 'جرين هيل', childPlaces: [] }] }] };
الآن لنفترض أنك تريد إضافة زر لحذف مكان زرته بالفعل. كيف ستقوم بذلك؟ تحديث الحالة المتداخلة يتطلب تحديث الحالة المتداخلة عمل نسخ للكائنات بدءًا من الجزء الذي تغيّر وصولاً إلى أكبر كائن أب. حذف مكان متداخل بعمق يتطلب نسخ السلسلة كاملة، وهذا يجعل الكود طويلاً جدًا
إذا كانت الحالة متداخلة جدًا لدرجة يصعب تحديثها، فكر في جعلها مسطحة. إليك طريقة لإعادة هيكلة البيانات: بدلاً من استخدام هيكل شجري حيث يحتوي كل عنصر على مصفوفة من العناصر الفرعية، يمكنك جعل كل عنصر يحتوي على مصفوفة من معرّفات (ids) العناصر الفرعية. ثم خزّن خريطة تربط كل معرّف عنصر بالمكان الفعلي المقابل له.
اعادة الهيكلة هذه قد تذكرك بجدول قاعدة البيانات:
export const initialTravelPlan = { 0: { id: 0, title: '(Root)', childIds: [1, 42, 46], }, 1: { id: 1, title: 'الأرض', childIds: [2, 10, 19, 26, 34] }, 2: { id: 2, title: 'افريقيا', childIds: [3, 4, 5, 6 , 7, 8, 9] }, 3: { id: 3, title: 'بوتسوانا', childIds: [] }, 4: { id: 4, title: 'مصر', childIds: [] }, 5: { id: 5, title: 'كينيا', childIds: [] }, 6: { id: 6, title: 'مدغشقر', childIds: [] }, 7: { id: 7, title: 'المغرب', childIds: [] }, 8: { id: 8, title: 'نيجيريا', childIds: [] }, 9: { id: 9, title: 'افريقيا', childIds: [] }, 10: { id: 10, title: 'الأمريكتين', childIds: [11, 12, 13, 14, 15, 16, 17, 18], }, 11: { id: 11, title: 'الأرجنتين', childIds: [] }, 12: { id: 12, title: 'البرازيل', childIds: [] }, 13: { id: 13, title: 'بربادوس', childIds: [] }, 14: { id: 14, title: 'كندا', childIds: [] }, 15: { id: 15, title: 'جامايكا', childIds: [] }, 16: { id: 16, title: 'المكسيك', childIds: [] }, 17: { id: 17, title: 'ترينيداد وتوباغو', childIds: [] }, 18: { id: 18, title: 'فنزويلا', childIds: [] }, 19: { id: 19, title: 'آسيا', childIds: [20, 21, 22, 23, 24, 25], }, 20: { id: 20, title: 'الصين', childIds: [] }, 21: { id: 21, title: 'الهند', childIds: [] }, 22: { id: 22, title: 'سنغافورة', childIds: [] }, 23: { id: 23, title: 'كوريا الجنوبية', childIds: [] }, 24: { id: 24, title: 'تايلاند', childIds: [] }, 25: { id: 25, title: 'فيتنام', childIds: [] }, 26: { id: 26, title: 'أوروبا', childIds: [27, 28, 29, 30, 31, 32, 33], }, 27: { id: 27, title: 'كرواتيا', childIds: [] }, 28: { id: 28, title: 'فرنسا', childIds: [] }, 29: { id: 29, title: 'ألمانيا', childIds: [] }, 30: { id: 30, title: 'إيطاليا', childIds: [] }, 31: { id: 31, title: 'البرتغال', childIds: [] }, 32: { id: 32, title: 'إسبانيا', childIds: [] }, 33: { id: 33, title: 'تركيا', childIds: [] }, 34: { id: 34, title: 'أوقيانوسيا', childIds: [35, 36, 37, 38, 39, 40, 41], }, 35: { id: 35, title: 'أستراليا', childIds: [] }, 36: { id: 36, title: 'بورا بورا (بولينيزيا الفرنسية)', childIds: [] }, 37: { id: 37, title: 'جزيرة القيامة (الشيلي)', childIds: [] }, 38: { id: 38, title: 'فيجي', childIds: [] }, 39: { id: 40, title: 'هاواي (ولايات متحدة الامريكية)', childIds: [] }, 40: { id: 40, title: 'نيوزلندا', childIds: [] }, 41: { id: 41, title: 'فانواتو', childIds: [] }, 42: { id: 42, title: 'القمر', childIds: [43, 44, 45] }, 43: { id: 43, title: 'ريتا', childIds: [] }, 44: { id: 44, title: 'بيكولوميني', childIds: [] }, 45: { id: 45, title: 'تايكو', childIds: [] }, 46: { id: 46, title: 'المريخ', childIds: [47, 48] }, 47: { id: 47, title: 'مدينة الذرة', childIds: [] }, 48: { id: 48, title: 'جرين هيل', childIds: [] } };
الآن بعد أن أصبحت الحالة “مسطحة” (المعروفة أيضًا بـ “المنظمة”)، أصبح تحديث العناصر المتداخلة أسهل.
لحذف مكان الآن، تحتاج فقط إلى تحديث مستويين من الحالة:
- يجب أن تُحدث النسخة المعدلة من المكان الأب لتستثني معرّف المكان المحذوف من مصفوفة
childIds
. - يجب أن تتضمن النسخة المعدلة من كائن
table
الجذري النسخة المحدثة من المكان الأب.
إليك مثال على كيفية القيام بذلك:
import { useState } from 'react'; import { initialTravelPlan } from './places.js'; export default function TravelPlan() { const [plan, setPlan] = useState(initialTravelPlan); function handleComplete(parentId, childId) { const parent = plan[parentId]; // أنشئ نسخة جديدة من المكان الأب // لا تشمل معرّف الطفل هذا. const nextParent = { ...parent, childIds: parent.childIds .filter(id => id !== childId) }; // حدث الكائن "root" setPlan({ ...plan, // ...لكي يتضمن النسخة المحدثة من المكان الأب. [parentId]: nextParent }); } const root = plan[0]; const planetIds = root.childIds; return ( <> <h2>أماكن لزيارة</h2> <ol> {planetIds.map(id => ( <PlaceTree key={id} id={id} parentId={0} placesById={plan} onComplete={handleComplete} /> ))} </ol> </> ); } function PlaceTree({ id, parentId, placesById, onComplete }) { const place = placesById[id]; const childIds = place.childIds; return ( <li> {place.title} <button onClick={() => { onComplete(parentId, id); }}> انتهى </button> {childIds.length > 0 && <ol> {childIds.map(childId => ( <PlaceTree key={childId} id={childId} parentId={id} placesById={placesById} onComplete={onComplete} /> ))} </ol> } </li> ); }
يمكنك تداخل الحالة بقدر ما ترغب، ولكن جعلها “مسطحة” يمكن أن يحل العديد من المشكلات. فهي تجعل تحديث الحالة أسهل، وتساعد على التأكد من عدم وجود تكرار في أجزاء مختلفة من كائن متداخل.
Deep Dive
من المثالي أيضًا أن تقوم بإزالة العناصر المحذوفة (وأطفالها!) من كائن “الجدول” لتحسين استخدام الذاكرة. هذه النسخة تقوم بذلك. كما أنها تستعمل خاصية Immer to make the update logic more concise. لتجعل تحديث المنطق اكثر دقة
{ "dependencies": { "immer": "1.7.3", "react": "latest", "react-dom": "latest", "react-scripts": "latest", "use-immer": "0.5.1" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" }, "devDependencies": {} }
في بعض الأحيان، يمكنك تقليل تداخل الحالة عن طريق نقل بعض الحالات المتداخلة إلى المكونات الفرعية. هذه الطريقة مفيدة لحالات واجهة المستخدم المؤقتة التي لا تحتاج إلى تخزين، مثل حالة تمرير الماوس فوق عنصر
Recap
- إذا كانت متغيرات الحالة تتغير دائمًا معًا، فكر في دمجها في متغير واحد.
- اختر متغيرات الحالة بعناية لتجنب إنشاء حالات مستحيلة.
- نظم حالتك بطريقة تقلل من فرص ارتكاب الأخطاء أثناء تحديثها.
- تجنب الحالة الزائدة والتكرار حتى لا تحتاج إلى مزامنتها.
- لا تضع الخصائص في الحالة إلا إذا كنت ترغب تحديدًا في منع التحديثات.
- لأنماط واجهة المستخدم مثل الاختيار، احتفظ بالمعرّف أو الفهرس في الحالة بدلاً من الكائن نفسه.
- إذا كان تحديث الحالة المتداخلة بعمق معقدًا، جرب تسويتها.
Challenge 1 of 4: إصلاح مكون لا يتم تحديثه
المكون التالي Clock
يستقبل خاصيتين: color
و time
.
عندما تختار لون مختلف في صندوق الاختيار, المكون Clock
يستقبل خاصية color
مختلفة من مكون الأب. ولكن لسبب ما, اللون المعروض لا يتم تحديثه. لماذا؟ قم بحل المشكلة.
import { useState } from 'react'; export default function Clock(props) { const [color, setColor] = useState(props.color); return ( <h1 style={{ color: color }}> {props.time} </h1> ); }