اختيار بُنية State
بُنية state الجيدة يمكن أن تُحدث فرقًا بين مكون يسهل تعديله وتنقيح أخطائه، ومكون يكون مصدرًا دائمًا للأخطاء. إليك بعض النصائح التي يجب عليك مراعاتها عند بُنية state.
You will learn
- متى تستخدم متغير state واحد مقابل متغيرات state متعددة
- ما يجب تجنبه عند تنظيم state
- كيفية إصلاح المشاكل الشائعة مع بُنية state
مبادئ بُنية state
عندما تكتب مكونًا يحتوي على بعض state، سيتعين عليك اتخاذ قرارات حول عدد متغيرات state التي يجب استخدامها وما يجب أن يكون شكل بياناتها. على الرغم من أنه من الممكن كتابة برامج صحيحة حتى مع بُنية state غير مثالية، إلا أن هناك بعض المبادئ التي يمكن أن ترشدك لاتخاذ خيارات أفضل:
- اجمع state المرتبطة. إذا كنت تُحدّث دائمًا متغيري state أو أكثر في نفس الوقت، ففكر في دمجهما في متغير state واحد.
- تجنب التناقضات في state. عندما يتم بُنية state بطريقة قد تتناقض فيها عدة أجزاء من state وتكون “غير متفقة” مع بعضها البعض، فإنك تترك مجالًا للأخطاء. حاول تجنب ذلك.
- تجنب state الزائدة. إذا كان بإمكانك حساب بعض المعلومات من props المكون أو من متغيرات state الموجودة أثناء rendering، فلا يجب عليك وضع تلك المعلومات في state ذلك المكون.
- تجنب التكرار في state. عندما يتم تكرار نفس البيانات بين متغيرات state متعددة، أو ضمن objects متداخلة، يكون من الصعب الحفاظ على تزامنها. قلل من التكرار عندما تستطيع.
- تجنب state المتداخلة بعمق. state الهرمية العميقة ليست مريحة جدًا للتحديث. عندما يكون ذلك ممكنًا، فضّل بُنية state بطريقة مسطحة.
الهدف من وراء هذه المبادئ هو جعل state سهلة التحديث دون إدخال أخطاء. إزالة البيانات الزائدة والمكررة من state تساعد على ضمان أن جميع أجزائها تظل متزامنة. هذا مشابه لكيفية رغبة مهندس قاعدة بيانات في “تطبيع بُنية قاعدة البيانات” لتقليل فرصة الأخطاء. لإعادة صياغة ألبرت أينشتاين، “اجعل state الخاصة بك بسيطة قدر الإمكان - ولكن ليس أبسط من ذلك.”
الآن دعنا نرى كيف تنطبق هذه المبادئ عمليًا.
اجمع state المرتبطة
قد تكون في بعض الأحيان غير متأكد بين استخدام متغير state واحد أو متغيرات state متعددة.
هل يجب عليك فعل هذا؟
const [x, setX] = useState(0); const [y, setY] = useState(0);
أم هذا؟
const [position, setPosition] = useState({ x: 0, y: 0 });
من الناحية التقنية، يمكنك استخدام أي من هذين النهجين. ولكن إذا كان بعض متغيري state يتغيران دائمًا معًا، فقد يكون من الجيد توحيدهما في متغير state واحد. عندها لن تنسى أن تحافظ عليهما متزامنين دائمًا، كما في هذا المثال حيث تحريك المؤشر يُحدّث كلا الإحداثيات للنقطة الحمراء:
حالة أخرى حيث ستجمع البيانات في object أو array هي عندما لا تعرف عدد أجزاء state التي ستحتاجها. على سبيل المثال، يكون ذلك مفيدًا عندما يكون لديك نموذج حيث يمكن للمستخدم إضافة حقول مخصصة.
تجنب التناقضات في state
إليك نموذج ملاحظات فندق مع متغيري state isSending و isSent:
While this code works, it leaves the door open for “impossible” states. For example, if you forget to call setIsSent and setIsSending together, you may end up in a situation where both isSending and isSent are true at the same time. The more complex your component is, the harder it is to understand what happened.
على الرغم من أن هذا الكود يعمل، إلا أنه يترك الباب مفتوحًا لحالات “مستحيلة”. على سبيل المثال، إذا نسيت استدعاء setIsSent و setIsSending معًا، فقد تنتهي في موقف حيث يكون كل من isSending و isSent true في نفس الوقت. كلما كان مكونك أكثر تعقيدًا، كان من الصعب فهم ما حدث.
Since isSending and isSent should never be true at the same time, it is better to replace them with one status state variable that may take one of three valid states: 'typing' (initial), 'sending', and 'sent':
نظرًا لأن isSending و isSent يجب ألا يكونا true في نفس الوقت أبدًا، فمن الأفضل استبدالهما بمتغير state واحد status يمكن أن يأخذ واحدة من ثلاث حالات صالحة: 'typing' (الأولية)، 'sending'، و 'sent':
You can still declare some constants for readability:
const isSending = status === 'sending'; const isSent = status === 'sent';
But they’re not state variables, so you don’t need to worry about them getting out of sync with each other.
لا يزال بإمكانك الإعلان عن بعض الثوابت من أجل قابلية القراءة:
const isSending = status === 'sending'; const isSent = status === 'sent';
لكنها ليست متغيرات state، لذا لا داعي للقلق بشأن خروجها عن التزامن مع بعضها البعض.
تجنب state الزائدة
إذا كان بإمكانك حساب بعض المعلومات من props المكون أو من متغيرات state الموجودة أثناء rendering، فـيجب ألا تضع تلك المعلومات في state ذلك المكون.
على سبيل المثال، خذ هذا النموذج. إنه يعمل، ولكن هل يمكنك العثور على أي state زائدة فيه؟
This form has three state variables: firstName, lastName, and fullName. However, fullName is redundant. You can always calculate fullName from firstName and lastName during render, so remove it from state.
This is how you can do it:
هذا النموذج يحتوي على ثلاثة متغيرات state: firstName، lastName، و fullName. ومع ذلك، fullName زائدة. يمكنك دائمًا حساب fullName من firstName و lastName أثناء render، لذا أزله من state.
هكذا يمكنك فعل ذلك:
Here, fullName is not a state variable. Instead, it’s calculated during render:
const fullName = firstName + ' ' + lastName;
As a result, the change handlers don’t need to do anything special to update it. When you call setFirstName or setLastName, you trigger a re-render, and then the next fullName will be calculated from the fresh data.
هنا، fullName ليس متغير state. بدلاً من ذلك، يتم حسابه أثناء render:
const fullName = firstName + ' ' + lastName;
ونتيجة لذلك، لا تحتاج معالجات التغيير إلى فعل أي شيء خاص لتحديثه. عندما تستدعي setFirstName أو setLastName، فإنك تشغل re-render، وبعد ذلك سيتم حساب fullName التالي من البيانات الجديدة.
غوص عميق
مثال شائع على state الزائدة هو كود مثل هذا:
function Message({ messageColor }) { const [color, setColor] = useState(messageColor);
Here, a color state variable is initialized to the messageColor prop. The problem is that if the parent component passes a different value of messageColor later (for example, 'red' instead of 'blue'), the color state variable would not be updated! The state is only initialized during the first render.
This is why “mirroring” some prop in a state variable can lead to confusion. Instead, use the messageColor prop directly in your code. If you want to give it a shorter name, use a constant:
هنا، يتم تهيئة متغير state color بـ prop messageColor. المشكلة هي أنه إذا مرر المكون الأصلي قيمة مختلفة لـ messageColor لاحقًا (على سبيل المثال، 'red' بدلاً من 'blue')، فلن يتم تحديث متغير state color! يتم تهيئة state فقط أثناء render الأول.
هذا هو السبب في أن “نسخ” prop معين في متغير state يمكن أن يؤدي إلى الارتباك. بدلاً من ذلك، استخدم prop messageColor مباشرة في الكود الخاص بك. إذا كنت تريد إعطائه اسمًا أقصر، فاستخدم ثابتًا:
function Message({ messageColor }) { const color = messageColor;
This way it won’t get out of sync with the prop passed from the parent component.
”Mirroring” props into state only makes sense when you want to ignore all updates for a specific prop. By convention, start the prop name with initial or default to clarify that its new values are ignored:
بهذه الطريقة لن يخرج عن التزامن مع prop الممرر من المكون الأصلي.
”نسخ” props إلى state له معنى فقط عندما تريد تجاهل كل التحديثات لـ prop معين. بحسب الاتفاقية، ابدأ اسم prop بـ initial أو default لتوضيح أن قيمه الجديدة تم تجاهلها:
function Message({ initialColor }) { // The `color` state variable holds the *first* value of `initialColor`. // Further changes to the `initialColor` prop are ignored. const [color, setColor] = useState(initialColor);
تجنب التكرار في state
تتيح قائمة القائمة هذه اختيار وجبة خفيفة واحدة من عدة خيارات للسفر:
Currently, it stores the selected item as an object in the selectedItem state variable. However, this is not great: the contents of the selectedItem is the same object as one of the items inside the items list. This means that the information about the item itself is duplicated in two places.
Why is this a problem? Let’s make each item editable:
حاليًا، تخزن العنصر المحدد ككائن في متغير state selectedItem. ومع ذلك، هذا ليس جيدًا: محتويات selectedItem هي نفس الكائن كأحد العناصر داخل قائمة items. هذا يعني أن المعلومات حول العنصر نفسه مكررة في مكانين.
لماذا هذه مشكلة؟ دعنا نجعل كل عنصر قابلاً للتحرير:
Notice how if you first click “Choose” on an item and then edit it, the input updates but the label at the bottom does not reflect the edits. This is because you have duplicated state, and you forgot to update selectedItem.
Although you could update selectedItem too, an easier fix is to remove duplication. In this example, instead of a selectedItem object (which creates a duplication with objects inside items), you hold the selectedId in state, and then get the selectedItem by searching the items array for an item with that ID:
لاحظ كيف أنه إذا نقرت أولاً على “Choose” على عنصر ثم قمت بتحريره، فإن الإدخال يتحدث لكن التسمية في الأسفل لا تعكس التعديلات. هذا لأن لديك state مكررة، ونسيت تحديث selectedItem.
على الرغم من أنه يمكنك تحديث selectedItem أيضًا، إلا أن الإصلاح الأسهل هو إزالة التكرار. في هذا المثال، بدلاً من كائن selectedItem (الذي ينشئ تكرارًا مع الكائنات داخل items)، فإنك تحتفظ بـ selectedId في state، ثم تحصل على selectedItem بالبحث في مصفوفة items عن عنصر بذلك ID:
The state used to be duplicated like this:
items = [{ id: 0, title: 'pretzels'}, ...]selectedItem = {id: 0, title: 'pretzels'}
But after the change it’s like this:
items = [{ id: 0, title: 'pretzels'}, ...]selectedId = 0
The duplication is gone, and you only keep the essential state!
Now if you edit the selected item, the message below will update immediately. This is because setItems triggers a re-render, and items.find(...) would find the item with the updated title. You didn’t need to hold the selected item in state, because only the selected ID is essential. The rest could be calculated during render.
كانت state مكررة مثل هذا:
items = [{ id: 0, title: 'pretzels'}, ...]selectedItem = {id: 0, title: 'pretzels'}
لكن بعد التغيير أصبحت مثل هذا:
items = [{ id: 0, title: 'pretzels'}, ...]selectedId = 0
التكرار اختفى، وأنت تحتفظ فقط بـ state الأساسية!
الآن إذا قمت بتحرير العنصر المحدد، ستتحدث الرسالة أدناه فورًا. هذا لأن setItems تشغل re-render، و items.find(...) ستجد العنصر بالعنوان المحدث. لم تكن بحاجة للاحتفاظ بـالعنصر المحدد في state، لأن فقط ID المحدد أساسي. الباقي يمكن حسابه أثناء render.
تجنب state المتداخلة بعمق
تخيل خطة سفر تتكون من كواكب وقارات ودول. قد تميل إلى بُنية state باستخدام objects ومصفوفات متداخلة، كما في هذا المثال:
Now let’s say you want to add a button to delete a place you’ve already visited. How would you go about it? Updating nested state involves making copies of objects all the way up from the part that changed. Deleting a deeply nested place would involve copying its entire parent place chain. Such code can be very verbose.
If the state is too nested to update easily, consider making it “flat”. Here is one way you can restructure this data. Instead of a tree-like structure where each place has an array of its child places, you can have each place hold an array of its child place IDs. Then store a mapping from each place ID to the corresponding place.
This data restructuring might remind you of seeing a database table:
الآن لنفترض أنك تريد إضافة زر لحذف مكان قد زرته بالفعل. كيف ستفعل ذلك؟ تحديث state المتداخلة يتضمن عمل نسخ من objects طوال الطريق من الجزء الذي تغير. حذف مكان متداخل بعمق سيتضمن نسخ سلسلة الأماكن الأصلية بأكملها. يمكن أن يكون مثل هذا الكود طويلاً جدًا.
إذا كانت state متداخلة جدًا للتحديث بسهولة، ففكر في جعلها “مسطحة”. إليك طريقة واحدة يمكنك إعادة تنظيم هذه البيانات بها. بدلاً من بُنية شبيهة بالشجرة حيث كل place له مصفوفة من أماكنه الفرعية، يمكنك جعل كل مكان يحتوي على مصفوفة من IDs أماكنه الفرعية. ثم احفظ تعيينًا من كل ID مكان إلى المكان المقابل.
قد تذكرك إعادة هيكلة البيانات هذه برؤية جدول قاعدة بيانات:
Now that the state is “flat” (also known as “normalized”), updating nested items becomes easier.
In order to remove a place now, you only need to update two levels of state:
- The updated version of its parent place should exclude the removed ID from its
childIdsarray. - The updated version of the root “table” object should include the updated version of the parent place.
Here is an example of how you could go about it:
الآن بعد أن أصبحت state “مسطحة” (تُعرف أيضًا باسم “منظمة”)، يصبح تحديث العناصر المتداخلة أسهل.
لإزالة مكان الآن، تحتاج فقط إلى تحديث مستويين من state:
- النسخة المحدثة من مكان الأصل يجب أن تستبعد ID المحذوف من مصفوفة
childIdsالخاصة به. - النسخة المحدثة من كائن “الجدول” الجذر يجب أن تتضمن النسخة المحدثة من المكان الأصل.
إليك مثال على كيفية القيام بذلك:
You can nest state as much as you like, but making it “flat” can solve numerous problems. It makes state easier to update, and it helps ensure you don’t have duplication in different parts of a nested object.
غوص عميق
بشكل مثالي، يجب عليك أيضًا إزالة العناصر المحذوفة (وأطفالها!) من كائن “الجدول” لتحسين استخدام الذاكرة. هذه النسخة تفعل ذلك. كما أنها تستخدم Immer لجعل منطق التحديث أكثر اختصارًا.
Sometimes, you can also reduce state nesting by moving some of the nested state into the child components. This works well for ephemeral UI state that doesn’t need to be stored, like whether an item is hovered.
أحيانًا، يمكنك أيضًا تقليل تداخل state عن طريق نقل بعض state المتداخلة إلى المكونات الفرعية. هذا يعمل بشكل جيد للـ state المؤقتة لواجهة المستخدم التي لا تحتاج إلى تخزينها، مثل ما إذا كان عنصر محومًا.
خلاصة
- إذا كان متغيران state يتحدثان دائمًا معًا، ففكر في دمجهما في متغير واحد.
- اختر متغيرات state الخاصة بك بعناية لتجنب إنشاء حالات “مستحيلة”.
- قم ببُنية state الخاصة بك بطريقة تقلل من فرص ارتكاب خطأ في تحديثها.
- تجنب state الزائدة والمكررة حتى لا تضطر إلى الحفاظ على تزامنها.
- لا تضع props في state إلا إذا كنت تريد على وجه التحديد منع التحديثات.
- لنماذج واجهة المستخدم مثل التحديد، احفظ ID أو الفهرس في state بدلاً من الكائن نفسه.
- إذا كان تحديث state المتداخلة بعمق معقدًا، فحاول تسطيحها.
تحدي 1 من 4: أصلح مكونًا لا يتحدث
يستقبل مكون Clock هذا prop: color و time. عندما تحدد لونًا مختلفًا في مربع التحديد، يستقبل مكون Clock prop color مختلفة من مكونه الأصل. ومع ذلك، لسبب ما، لا يتحدث اللون المعروض. لماذا؟ أصلح المشكلة.