اختيار هيكلة الحالة

إن هيكلة للحالة (state) بشكل صحيح هي أحد العناصر الأساسية التي يمكن أن تجعل المكون (component) سهل التعديل والتصحيح عند مواجهة الأخطاء، أو بالعكس، قد تكون مصدراً للتعقيد والمشاكل في مشروعك. لذا، إليك بعض النصائح التي يمكنك اتباعها لهيكلة الحالات (state) بشكل فعّال:

You will learn

  • متى تستخدم متغير حالة واحد مقابل عدة متغيرات حالة
  • ما يجب تجنبه عند تنظيم الحالة
  • كيفية إصلاح المشكلات الشائعة في هيكلة الحالة

مبادئ هيلكة الحالات

عندما تقوم بكتابة مكون يحتوي على حالة، ستحتاج إلى اتخاذ قرارات بشأن عدد متغيرات الحالة التي ستستخدمها وشكل البيانات التي تحتويها. بينما من الممكن كتابة برامج صحيحة حتى مع هيكلة حالة غير مثلى، هناك بعض المبادئ التي يمكن أن تساعدك في اتخاذ قرارات أفضل:

  1. دمج الحالات المتشابهة اذا كنت تحدث اثنين او اكثر من الحالات في آن واحدة. فكر في دمجهم الى حالة واحدة
  2. تجنب تناقضات في الحالة. عندما تكون الحالة مهيكلة بطريقة يكون فيها اجزاء من الحالة متناقضة, فهذا يفتح مجال للأخطاء. حاول تجنبها
  3. تجنب الحالة الزائدة. اذا كنت تعالج معلومة ما عن طريق خصائص (prop) او الحالة موجودة مسبقا. فيجب عليك تفادي وضع تلك المعلومة في حالات ذلك المكون
  4. تجنب تكرار الحالات. عندما تكون نفس البيانات مكررة بين عدة حالات، أو ضمن كائنات متداخلة، يصبح من الصعب الحفاظ على تزامنها. تفادى التكرار قدر الإمكان.
  5. تجنب الحالة المتداخلة بعمق. تجنب الحالة المتداخلة بعمق. الحالة ذات الهيكلية العميقة ليست مناسبة للتحديث. يُفضّل هيكلة الحالة بطريقة مسطحة.

الهدف من هذه المبادئ هو تسهيل تحديث الحالة دون فتح مجال للأخطاء. من خلال إزالة البيانات الزائدة والمتكررة من الحالة، نضمن تماسك جميع أجزائها. هذا مماثل لما قد يفعله مهندس قاعدة المعلومات “تطبيع” لهيكلة قاعدة المعلومات لتقليل من الأخطاء المحتملة. كما قال أينشتاين, “اجعل حالتك بسيطة بقدر الإمكان—لكن لا تجعلها أبسط من ذلك.”

لنرى الآن كيف يمكن تطبيق هذه المبادئ عملياً.

قد تكون أحياناً غير متأكد مما إذا كنت ينبغي أن تستخدم متغير حالة واحداً أم عدة متغيرات حالة.

هل يجب عليك فعل هذا؟

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>
  )
}

في حالة أخرى، قد تحتاج إلى جمع بيانات الحالة في كائن أو مصفوفة عندما لا تعرف عدد أجزاء الحالة التي ستحتاجها. على سبيل المثال، ستكون هذه الطريقة مفيدة عندما يكون لديك نموذج يتيح للمستخدم إضافة حقول مخصصة

Pitfall

إذا كان متغير الحالة الخاص بك هو كائن، فتذكر أنه لا يمكنك تحديث حقل واحد فقط فيه دون نسخ الحقول الأخرى بشكل صريح. على سبيل المثال، لا يمكنك استخدام setPosition({ x: 100 }) في المثال أعلاه لأنه لن يحتوي على خاصية y على الإطلاق! بدلاً من ذلك، إذا كنت ترغب في تعيين x فقط، يمكنك إما استخدام setPosition({ ...position, x: 100 })، أو تقسيمها إلى متغيرين للحالة واستخدام setX(100).

تجنب التناقضات في حالات

هنا نموذج تقييم للفندق يحتوي على متغيري حالة 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>
  );
}