اذهب إلى المحتوى

حالات أعقد للمكونات وتنقيح تطبيقات React


ابراهيم الخضور

حالة أكثر تعقيدًا

كانت الحالة التي صادفتها في تطبيقنا السابق بسيطة لكونها متعلقة بعدد صحيح واحد. لكن ما العمل إن تطلب أحد التطبيقات التعامل مع حالة أكثر تعقيدًا؟ إنّ أكثر الطرق سهولة في إنجاز ذلك، هو استخدام دالة useState عدة مرات بحيث تتجزأ الحالة الكلية إلى "قطع" صغيرة. سنبني في الشيفرة التالية حالة من قطعتين لتطبيقنا تسمى الأولى "left" والأخرى "right" وستأخذ كل منهما 0 كقيمة ابتدائية.

const App = (props) => {
  const [left, setLeft] = useState(0)
  const [right, setRight] = useState(0)

  return (
    <div>
      <div>
        {left}
        <button onClick={() => setLeft(left + 1)}>
          left
        </button>
        <button onClick={() => setRight(right + 1)}>
          right
        </button>
        {right}
      </div>
    </div>
  )
}

سنمنح المكوّن وصولًا إلى الدالتين setLeft وsetRight اللتان ستُستخدمَان لتحديث قطعتي الحالة. يمكننا تحقيق غرض التطبيق بحفظ عدد النقرات التي تحصل على كلا الزرين "left" و"right" ضمن كائن مفرد:

{
  left: 0,
  right: 0
}

وبهذا سيبدو التطبيق كالتالي:

const App = (props) => {
  const [clicks, setClicks] = useState({
    left: 0, right: 0
  })

  const handleLeftClick = () => {
    const newClicks = { 
      left: clicks.left + 1, 
      right: clicks.right 
    }
    setClicks(newClicks)
  }

  const handleRightClick = () => {
    const newClicks = { 
      left: clicks.left, 
      right: clicks.right + 1 
    }
    setClicks(newClicks)
  }

  return (
    <div>
      <div>
        {clicks.left}
        <button onClick={handleLeftClick}>left</button>
        <button onClick={handleRightClick}>right</button>
        {clicks.right}
      </div>
    </div>
  )
}

يمتلك المكوّن الآن قطعة واحدة من الحالة، وستقع على عاتق معالجات الأحداث مهمة تغيير الحالة لكامل التطبيق. يبدو معالج الحدث فوضويًا بعض الشيء. فعندما يُنقر الزر "left" تُستدعى الدالة التالية:

const handleLeftClick = () => {
  const newClicks = { 
    left: clicks.left + 1, 
    right: clicks.right 
  }
  setClicks(newClicks)
}

وتُخزّن القيم الجديدة لحالة التطبيق ضمن الكائن التالي:

{
  left: clicks.left + 1,
  right: clicks.right
}

ستحمل الخاصية left القيمة left+1، بينما ستبقى قيمة الخاصية right على ما كانت عليه في الحالة السابقة. يمكننا تعريف كائن الحالة الجديد بشكل أكثر أناقة مستخدمين صيغة توسيع الكائن (object spread) التي أضيفت إلى اللغة صيف 2018:

const handleLeftClick = () => {
  const newClicks = { 
    ...clicks, 
    left: clicks.left + 1 
  }
  setClicks(newClicks)
}

const handleRightClick = () => {
  const newClicks = { 
    ...clicks, 
    right: clicks.right + 1 
  }
  setClicks(newClicks)
}

تبدو الصيغة غريبة بعض الشيء. حيث تنشئ العبارة {clicks... } كائنًا جديدًا يحمل نسخًا عن كل الخصائص التي يمتلكها الكائن clicks. فعندما نحدد خاصية معينة مثل right في العبارة {clicks,right: 1...}، ستكون القيمة الجديدة للخاصية right تساوي 1. لو تأملنا العبارة التالية في المثال السابق:

 {...clicks, right: clicks.right + 1 }

سنجد أنها تنشئ نسخًا للكائن clicks، ستزداد فيها قيم الخاصية right بمقدار واحد. لا حاجة إلى إسناد قيمة الخاصية إلى متغيّر في معالج الحدث وبالتالي سنبسط كتابة الدوال إلى الشكل التالي:

const handleLeftClick = () =>
  setClicks({ ...clicks, left: clicks.left + 1 })

const handleRightClick = () =>
  setClicks({ ...clicks, right: clicks.right + 1 })

قد يتساءل بعض القراء لماذا لم نحدّث الحالة مباشرة كما يلي:

const handleLeftClick = () => {
  clicks.left++
  setClicks(clicks)
}

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

التعامل مع المصفوفات Handling arrays

لنضف قطعة من الحالة إلى تطبيقنا. تتذكر المصفوفة allClicks كل نقرة على زر حدثت في التطبيق.

const App = (props) => {
  const [left, setLeft] = useState(0)
  const [right, setRight] = useState(0)
  const [allClicks, setAll] = useState([])
  const handleLeftClick = () => {    
    setAll(allClicks.concat('L'))
    setLeft(left + 1)
  }
  const handleRightClick = () => {
    setAll(allClicks.concat('R'))
    setRight(right + 1)
  }
  return (
    <div>
      <div>
        {left}
        <button onClick={handleLeftClick}>left</button>
        <button onClick={handleRightClick}>right</button>
        {right}
        <p>{allClicks.join(' ')}</p>
      </div>
    </div>
  )
}

ستُخزّن كل نقرة في القطعة allClicks والتي هُيِّئت على شكل مصفوفة فارغة:

const [allClicks, setAll] = useState([])

سيُضاف الحرف 'L' إلى المصفوفة allClicks عندما يُنقر الزر "left":

const handleLeftClick = () => {
  setAll(allClicks.concat('L'))
  setLeft(left + 1)
}

إذًا فقطعة الحالة التي أنشأناها هي مصفوفة تضم كل عناصر الحالة السابقة بالإضافة إلى الحرف 'L'. وتتم إضافة العناصر الجديدة باستخدام التابع concat الذي لا يعدل في المصفوفة، بل يعيد مصفوفةً جديدةً يضاف إليها العنصر الجديد. وكما ذكرنا سابقًا، يمكننا إضافة العناصر إلى مصفوفة باستخدام التابع push، حيث يمكن دفع العنصر داخل مصفوفة الحالة ثم تحديثها وسيعمل التطبيق:

const handleLeftClick = () => {
  allClicks.push('L')
  setAll(allClicks)
  setLeft(left + 1)
}

لكن، لا تقم بذلك. فكما أشرنا سابقًا، لا يجب تغيير حالة مكوّنات React مثل allClicks مباشرةً. وحتى لو بدا الأمر ممكنًا، فقد يقودك إلى أخطاء من الصعب جدًا تنقيحها. لنلق الآن نظرة أقرب إلى الآلية التي يُصيّر بها التسلسل الزمني للنقرات:

const App = (props) => {
  // ...

  return (
    <div>
      <div>
        {left}
        <button onClick={handleLeftClick}>left</button>
        <button onClick={handleRightClick}>right</button>
        {right}
        <p>{allClicks.join(' ')}</p>
      </div>
    </div>
  )
}

استدعت الشيفرة السابقة التابع join الذي يجعل كل عناصر المصفوفة ضمن سلسلة نصية واحدة يفصل بينها فراغ وهو المعامل الذي مُرِّر إلى التابع.

التصيير الشرطي Conditional rendering

لنعدّل التطبيق بحيث يتولى الكائن الجديد History أمر تصيير تاريخ النقر على الأزرار:

const History = (props) => {
  if (props.allClicks.length === 0) {
    return (
      <div>
        the app is used by pressing the buttons
      </div>
    )
  }

  return (
    <div>
      button press history: {props.allClicks.join(' ')}
    </div>
  )
}

const App = (props) => {
  // ...

  return (
    <div>
      <div>
        {left}
        <button onClick={handleLeftClick}>left</button>
        <button onClick={handleRightClick}>right</button>
        {right}
        <History allClicks={allClicks} />
      </div>
    </div>
  )
}

سيعتمد الآن سلوك التطبيق على أحداث النقر على الأزرار. إن لم تُنقر أية أزرار، سيعني ذلك أن مصفوفة الحالة allClicks فارغة. سيُصيّر عندها المكوّن العنصر div وقد ظهرت ضمنه تعليمات للمستخدم:

<div>the app is used by pressing the buttons</div>
//..يستخدم التطبيق بالنقر على الأزرار

بقية الحالات سيصيّر المكوّن تاريخ النقر على الأزرار:

<div>
  button press history: {props.allClicks.join(' ')}
</div>

يصيّر المكوّن History مكوّنات React مختلفةً كليًا وفقًا لحالة التطبيق. يدعى هذا بالتصيير الشرطي. تقدم React طرقًا عدة للتصيير الشرطي والتي سنلقي عليها نظرةً أقرب في القسم 2 لاحقًا من هذه السلسلة. سنجري تعديلًا أخيرًا على تطبيقنا بإعادة صياغته مستخدمين المكوّن Button الذي عرّفناه سابقًا:

const History = (props) => {
  if (props.allClicks.length === 0) {
    return (
      <div>
        the app is used by pressing the buttons
      </div>
    )
  }

  return (
    <div>
      button press history: {props.allClicks.join(' ')}
    </div>
  )
}

const Button = ({ onClick, text }) => (  
  <button onClick={onClick}>
    {text}
  </button>
)
const App = (props) => {
  const [left, setLeft] = useState(0)
  const [right, setRight] = useState(0)
  const [allClicks, setAll] = useState([])

  const handleLeftClick = () => {
    setAll(allClicks.concat('L'))
    setLeft(left + 1)
  }

  const handleRightClick = () => {
    setAll(allClicks.concat('R'))
    setRight(right + 1)
  }

  return (
    <div>
      <div>
        {left}
        <Button onClick={handleLeftClick} text='left' />
        <Button onClick={handleRightClick} text='right' />
        {right}
        <History allClicks={allClicks} />
      </div>
    </div>
  )
}

كلمة حول React القديمة

نستخدم في هذا المنهاج state hook لإضافة حالة إلى مكوّنات React، حيث تعتبر الخطافات جزءًا من نسخ React الأحدث وتتوفر ابتداء من النسخة 16.8.0. لم تكن هناك طرق لإضافة حالة إلى دالة المكوّن قبل إضافة الخطافات، حيث عُرّفت المكوّنات التي تحتاج إلى حالة كصفوف class باستخدام الصيغة class. لقد قررنا في هذا المنهاج استخدام الخطافات منذ البداية لكي نضمن أننا نتعلم الأسلوب المستقبلي لمكتبة React. وعلى الرغم من أن المكوّنات المبنية على شكل دوال هي مستقبل React، لا يزال تعلم استخدام الأصناف مهمًا. فهناك المليارات من سطور الشيفرة المكتوبة بالأسلوب القديم، وقد ينتهي بك الأمر يومًا وأنت تنقح أحدها، أو قد تتعامل مع توثيق أو أمثلة عن React القديمة على الإنترنت. إذًا سنتعلم أكثر عن الأصناف لكن ليس الآن.

تنقيح تطبيقات React

يمضي المطورون وقتًا طويلًا في تنقيح وتصيير الشيفرات الموجودة، ويتوجب عليهم بين الفينة والأخرى كتابة أسطر جديدة. لكن بطبيعة الحال سنقضي جُلّ وقتنا باحثين عن سبب خطأ ما أو عن الطريقة التي يعمل بها مكوّن ما. لذلك فاقتناء أدوات للتنقيح والتمكّن من تحري الأخطاء أمر مهم للغاية. ولحسن الحظ تقدم مكتبة React عونًا منقطع النظير للمطورين فيما يتعلق بموضوع التنقيح. قبل أن نتابع، دعونا نتذكر القاعدة الأكثر أهمية في تطوير الويب، حيث تنص القاعدة الأولى في تطوير الويب على مايلي:

اقتباس

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

وعليك أن تبقي المحرر الذي تكتب فيه الشيفرة مفتوحًا في نفس الوقت مع صفحة الويب ودائمًا. وتذكر عندما تُخفق الشيفرة ويمتلئ المتصفح بالأخطاء أن لا تكتب المزيد من الشيفرة بل ابحث عن الخطأ وصححه مباشرةً.

fail_to_compile_01.png

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

const Button = ({ onClick, text }) => (
  <button onClick={onClick}>
    {text}
  </button>
)

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

const Button = (props) => { 
  console.log(props)  
  const { onClick, text } = props
  return (
    <button onClick={onClick}>
      {text}
    </button>
  )
}

سيكشف ذلك مباشرة أنّ اسم إحدى الخصائص مثلًا قد كُتب بطريقة خاطئة عند استخدام المكوّن.

ملاحظة: عندما تستخدم التعليمة console.log للتنقيح، لا تستخدم أسلوبًا مشابهًا لشيفرة Java. فلو أردت ضم معلومات مختلفة في الرسالة مثل استخدام إشارة (+). لا تكتب:

console.log('props value is ' + props)

بل افصل بين الأشياء التي تريد طباعتها بفاصلة ",":

console.log('props value is', props)

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

props value is [Object object]

بينما ستبقى العناصر التي تفصل بينها باستخدام الفاصلة واضحة على طرفية المتصفح. لا تُعتبر طريقة الولوج إلى الطرفية على أية حال هي الطريقة الوحيدة لتنقيح التطبيقات. إذ يمكنك إيقاف تنفيذ الشيفرة في منقح طرفية التطوير لمتصفح Chrome مثلًا باستخدام الأمر debugger في أي مكان ضمن الشيفرة. حيث سيتوقف التنفيذ عندما تصل إلى النقطة التي وضعت فيها الأمر السابق:

using_debugger_command_02.png

انتقل إلى النافذة Console لتتابع الحالة الراهنة للمتغيّرات بسهولة:

using_console_tab_03.png

يمكنك إزالة الأمر debugger وإعادة تحميل الصفحة حالما تكتشف الخطأ في شيفرتك. يمكّنك المنقح أيضًا من تنفيذ الشيفرة سطرًا سطرًا باستخدام عناصر التحكم الخاصة الموجودة على يمين النافذة source في الطرفية. لا حاجة لاستخدام الأمر debugger للوصول إلى المنقح، يمكنك عوضًا عن ذلك استخدام نقاط التوقف (break point) الموجودة في النافذة source، وتتبّع قيم متغيرات المكوّن في القسم Scope:

console_source_window_04.png

ننصحك بشدة أن تضيف إضافة مُوسِّعة (extension) لمكتبة React على متصفح Chrome تدعى React developer tools، الذي سيضيف النافذة الجديدة React إلى الطرفية:

react_extension_05.png

سنستخدم أدوات تطوير React الجديدة في تتبع حالة العناصر المختلفة في التطبيق وخصائصها. لكن النسخة الحالية من هذا الموسِّع تُغفل عرض المطلوب عن حالة المكوّنات التي تنشئها الخطافات:

react_extension_hooks_06.png

عُرّفت حالة المكوّن بالشكل التالي:

const [left, setLeft] = useState(0)
const [right, setRight] = useState(0)
const [allClicks, setAll] = useState([])

تُظهر أدوات التطوير حالة الخطافات حسب تسلسل تعريفها:

hooks_state_order_07.png

قواعد استخدام الخطافات Rules of Hooks

هناك قواعد وحدود معينة يجب أن نتقيد بها لنتأكد أن الحالة التي تعتمد على الخطافات في التطبيق ستعمل كما يجب. فلا يجب استدعاء التابع useState (وكذلك التابع useEffect الذي سنتعرف عليه لاحقًا) من داخل الحلقات أو العبارت الشرطية أو من أي موقع لا يمثل دالة لتعريف مكوّن. ذلك لنضمن أن الخطافات ستُستدعى وفق الترتيب ذاته لتعريفها وإلا سيتصرف التطبيق بشكل فوضوي. باختصار استدع الخطافات من داخل جسم الدوال المعرِّفة للمكوّنات:

const App = (props) => {
  // الشيفرة التالية صحيحة
  const [age, setAge] = useState(0)
  const [name, setName] = useState('Juha Tauriainen')

  if ( age > 10 ) {
    // هذه الشيفرة لن تعمل
    const [foobar, setFoobar] = useState(null)
  }

  for ( let i = 0; i < age; i++ ) {
    // هذه غير جيدة أيضًا
    const [rightWay, setRightWay] = useState(false)
  }

  const notGood = () => {
    // هذه غير مشروعة
    const [x, setX] = useState(-1000)
  }

  return (
    //...
  )
}

عودة إلى معالجة الأحداث

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

const App = (props) => {
  const [value, setValue] = useState(10)

  return (
    <div>
      {value}
      <button>reset to zero</button>
    </div>
  )
}

ReactDOM.render(
  <App />, 
  document.getElementById('root')
)

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

<button onClick={'crap...'}>button</button>

ستحذرنا React من ذلك على الطرفية:

index.js:2178 Warning: Expected `onClick` listener to be a function, instead got a value of `string` type.
    in button (at index.js:20)
    in div (at index.js:18)
    in App (at index.js:27)

وكذلك لن يعمل المعالج التالي:

<button onClick={value + 1}>button</button>

حيث نعرف معالج الحدث في هذه الحالة بالعبارة value+1 التي ستعيد قيمة العملية، وستحذرنا React أيضًا:

index.js:2178 Warning: Expected `onClick` listener to be a function, instead got a value of `number` type.

هذه الطريقة لن تنفع أيضًا:

<button onClick={value = 0}>button</button>

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

<button onClick={console.log('clicked the button')}>
  button
</button>

ستُطبع العبارة المكتوبة داخل الدالة لمرة واحدة ولن يحدث بعدها شيء عند إعادة نقر الزر. لماذا لم يعمل معالج الحدث إذًا على الرغم من وجود الدالة console.log؟ المشكلة هنا أن معالج الحدث قد عُرّف كاستدعاء لدالة، أي أنه سيعيد قيمة الدالة والتي ستكون "غير محددة" في حالة console.log. ستُنفَّذ هذه الدالة تحديدًا عندما يُعاد تصيير المكوّن، ولهذا لم تظهر العبارة سوى مرة واحدة. ستخفق المحاولة التالية أيضًا:

<button onClick={setValue(0)}>button</button>

ستجعل معالج الحدث استدعاءً لدالة أيضًا في هذه المحاولة، لن يفلح ذلك. ستسبب هذه المحاولة تحديدًا مشكلة من نوع آخر. فعندما يُصيّر المكوّن، ستُنفَّذ الدالة (setvalue(0 والتي بدورها ستُعيد تصيير المكوّن. إعادة التصيير هذه تستدعي مجددًا (setvalue(0 وهذا ما يسبب حلقة لانهائية من الاستدعاءات. تُستدعى دالة محددة عند النقر على الزر كالتالي:

<button onClick={() => console.log('clicked the button')}>
  button
</button>

يمثل معالج الحدث الآن دالة معرفة بالطريقة السهمية (console.log('clicked the button <= (). فلن تُستدعى الآن أية دوال عندما يصيّر المكوّن، لأن معالج الحدث قد أسند إلى مرجع لدالة سهمية، ولن تُستدعى هذه الدالة إلا بعد النقر على الزر. يمكننا استخدام نفس الأسلوب لإضافة زر تصفير الحالة في التطبيق الذي بدأناه:

<button onClick={() => setValue(0)}>button</button>

لقد جعلت الشيفرة السابقة الدالة (setValue(0 <=() معالج الحدث للزر. لا يعتبر تعريف معالج الحدث مباشرة ضمن الخاصية onClick الطريقة الأفضل بالضرورة. حيث نجده أحيانًا معرفًا في مكان آخر. سنعرّف في النسخة التالية من التطبيق الدالة السهمية التي ستلعب دور معالج الحدث ضمن جسم المكوّن ومن ثم سنسندها إلى المتغيّر handleClick:

const App = (props) => {
  const [value, setValue] = useState(10)

  const handleClick = () =>
    console.log('clicked the button')

  return (
    <div>
      {value}
      <button onClick={handleClick}>button</button>
    </div>
  )
}

يعتبر المتغيّر handleClick مرجعًا للدالة السهمية، وسيُمرَّر هذا المرجع إلى الزر كقيمة للخاصية onClick:

<button onClick={handleClick}>button</button>

قد تضم دالة معالج الحدث أكثر من أمر، فلا بد عندها من وضع الأوامر بين قوسين معقوصين:

const App = (props) => {
  const [value, setValue] = useState(10)

  const handleClick = () => {
    console.log('clicked the button')
      setValue(0)
  }
  return (
    <div>
      {value}
      <button onClick={handleClick}>button</button>
    v>
  )
}

الدالة التي تعيد دالة

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

const App = (props) => {
  const [value, setValue] = useState(10)

  const hello = () => {
    const handler = () => console.log('hello world')
    return handler
  }
  return (
    <div>
      {value}
      <button onClick={hello()}>button</button>
    </div>
  )
}

ستعمل هذه الشيفرة بشكل صحيح على الرغم من مظهرها المعقد. يستدعي معالج الحدث هنا دالة:

<button onClick={hello()}>button</button>

لكننا قررنا سابقًا أن لا يكون معالج الحدث استدعاء لدالة، بل يجب أن يكون دالة أو مرجعًا لها. لماذا إذًا سيعمل المعالج في هذه الحالة؟ عندما يُصيّر المكوّن، ستُنفّذ الدالة التالية:

const hello = () => {
  const handler = () => console.log('hello world')

  return handler
}

إنّ القيمة المعادة من الدالة السهمية الأولى هي دالة سهمية أخرى أسندت إلى المتغيّر handler. فعندما تصيّر React السطر التالي:

<button onClick={hello()}>button</button>

ستُسنَد القيمة المعادة من ()hello إلى الخاصية onClick، وسيتحول السطر مبدئيًا إلى:

<button onClick={() => console.log('hello world')}>
  button
</button>

إذًا نجح الأمر لأن الدالة hello قد أعادت دالة وسيكون معالج الحدث دالة أيضًا. لكن ما المغزى من هذا المبدأ؟ لنغير الشيفرة قليلًا:

const App = (props) => {
  const [value, setValue] = useState(10)

  const hello = (who) => {
    const handler = () => {
      console.log('hello', who)
    }
  return handler
}
  return (
    <div>
      {value}
      <button onClick={hello('world')}>button</button>
      <button onClick={hello('react')}>button</button>
      <button onClick={hello('function')}>button</button>
    </div>
  )
}

يضم التطبيق الآن ثلاثة أزرار لكل منها معالج حدث تُعرّفه الدالة hello التي تقبل معاملًا. عُرّف الزر الأول كما يلي:

<button onClick={hello('world')}>button</button>

يٌنشأ معالج الحدث باستدعاء الدالة hello التي تعيد الدالة التالية:

() => {
  console.log('hello', 'world')
}

يُعرّف الزر التالي بالشكل:

<button onClick={hello('react')}>button</button>

سيعيد استدعاء الدالة (hello(react (التي تُنشئ معالج الحدث الخاص بالزر) مايلي:

() => {
  console.log('hello', 'react')
}

لاحظ أن كلا الزرين حصل على معالج حدث خاص به. فاستخدام الدوال التي تعيد دوال قد يساعد على تعريف الدوال المعمّمة التي يمكن تغييرها حسب الطلب باستخدام المعاملات. حيث تمثل الدالة hello التي أنشأت معالجات الأحداث مصنعًا لإنتاج معالجات أحداث يحددها المستخدم كما يشاء. يبدو تعريف الدالة التالي طويلًا نوعا ما:

const hello = (who) => {
  const handler = () => {
    console.log('hello', who)
  }

  return handler
}

لنحذف متغيّرات الدالة المساعدة ونعيد التابع الذي أنشأناه:

const hello = (who) => {
  return () => {
    console.log('hello', who)
  }
}

وطالما أن الدالة hello مؤلفة من عبارة برمجية واحدة، سنحذف القوسين المعقوصين أيضًا ونستخدم الشكل المختصر للدالة السهمية:

const hello = (who) =>
  () => {
    console.log('hello', who)
  }

أخيرًا لنكتب كل الأسهم على السطر نفسه:

const hello = (who) => () => {
  console.log('hello', who)
}

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

const App = (props) => {
  const [value, setValue] = useState(10)

  const setToValue = (newValue) => () => {
    setValue(newValue)
  }  
  return (
    <div>
      {value}
       <button onClick={setToValue(1000)}>thousand</button>
      <button onClick={setToValue(0)}>reset</button>
     <button onClick={setToValue(value + 1)}>increment</button>
    </div>
  )
}

عندما يُصيّر المكوّن، سيُنشأ الزر thousand:

<button onClick={setToValue(1000)}>thousand</button>

يضبط معالج الحدث ليعيد قيمة العبارة (setValue(1000، والتي تمثلها الدالة التالية:

() => {
  setValue(1000)
}

يُعرّف زر الزيادة بالطريقة:

<button onClick={setToValue(value + 1)}>increment</button>

يُنشأ معالج الحدث باستدعاء الدالة (setToValue(value + 1 والتي تستقبل قيمة متغير الحالة value كمعامل لها بعد زيادته بواحد. فلو كانت قيمة المتغير 10 سيكون معالج الحدث الذي سيُنشأ كالتالي:

() => {
  setValue(11)
}

لبس ضروريًا استخدام (دوال تعيد دوال) لتقديم وظائف كهذه، فلو أعدنا الدالة setToValue المسؤولة عن تحديث الحالة إلى دالة عادية:

const App = (props) => {
  const [value, setValue] = useState(10)

  const setToValue = (newValue) => {
    setValue(newValue)
  }

  return (
    <div>
      {value}
      <button onClick={() => setToValue(1000)}>
        thousand
      </button>
      <button onClick={() => setToValue(0)}>
        reset
      </button>
      <button onClick={() => setToValue(value + 1)}>
        increment
      </button>
    </div>
  )
}

يمكننا عندها تعريف معالج الحدث كدالة تستدعي الدالة setToValue باستخدام المعامل المناسب. وسيكون عندها معالج الحدث لتصفير الحالة من الشكل:

<button onClick={() => setToValue(0)}>reset</button>

وفي النهاية اختيار أي من الطريقتين السابقتين في تعريف معالج الحدث هو أمر شخصي.

تمرير معالجات الأحداث إلى المكوّنات الأبناء

لنجزِّء مكوّن الزر إلى أقسامه الرئيسية:

const Button = (props) => (
  <button onClick={props.handleClick}>
    {props.text}
  </button>
)

يحصل المكوّن على دالة معالج الحدث الخاصة به من الخاصية handleClick ويأخذ النص الذي سيظهر عليه من الخاصية text. إنّ استخدام المكوّن Button أمر سهل، لكن علينا التأكد من اسم الصفة قبل تمرير الخاصية إلى المكوّن.

button_component_08.png

لا تعرّف المكوّنات داخل المكوّنات

لنبدأ بإظهار قيمة التطبيق ضمن المكوّن Display الخاص به. سنعدل الشيفرة بتعريف مكوّن جديد داخل المكوّن App:

// هذا هو المكان الصحيح لتعريف المكون
const Button = (props) => (
  <button onClick={props.handleClick}>
    {props.text}
  </button>
)

const App = props => {
  const [value, setValue] = useState(10)

  const setToValue = newValue => {
    setValue(newValue)
  }

  // لا تعرف المكونات داخل المكونات
  const Display = props => <div>{props.value}</div>
  return (
    <div>
      <Display value={value} />
      <Button handleClick={() => setToValue(1000)} text="thousand" />
      <Button handleClick={() => setToValue(0)} text="reset" />
      <Button handleClick={() => setToValue(value + 1)} text="increment" />
    </div>
  )
}

سيعمل التطبيق كما يبدو، لكن لا تعرّف مكوّنًا ضمن آخر. لن يقدم لك ذلك أية فائدة وسيقود إلى مشاكل عديدة. إذًا دعونا ننقل المكوّن Display إلى مكانه الصحيح خارج دالة المكوّن App:

const Display = props => <div>{props.value}</div>

const Button = (props) => (
  <button onClick={props.handleClick}>
    {props.text}
  </button>
)

const App = props => {
  const [value, setValue] = useState(10)

  const setToValue = newValue => {
    setValue(newValue)
  }

  return (
    <div>
      <Display value={value} />
      <Button handleClick={() => setToValue(1000)} text="thousand" />
      <Button handleClick={() => setToValue(0)} text="reset" />
      <Button handleClick={() => setToValue(value + 1)} text="increment" />
    </div>
  )
}

مواد جديرة بالقراءة

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

  • دروس ومقالات حول React من أكاديمية حسوب
  • توثيقات React الرسمية: تستحق هذه التوثيقات المطالعة في مرحلة ما، حيث يغدو لمعظم محتوياتها أهمية في سياق المنهاج، ماعدا تلك المتعلقة بالمكوّنات المعتمدة على الأصناف.
  • بعض مناهج Egghead.io الأجنبية: مثل Start learning React وتتمتع بقيمة عالية، وقد جرى تحديثها مؤخرًا. ويعتبر كذلك The Beginner's Guide to React جيدًا نوعًا ما. حيث يقدم المنهاجان مبادئ سنستعرضها في منهاجنا لاحقًا. وانتبه إلى أن المنهاج الأول يستخدم مكوّنات الأصناف، بينما يستخدم الثاني طريقة الدوال الجديدة.

التمارين 1.6- 1.14

سلّم حلول التمارين برفع الشيفرة إلى GitHub ثم أشر إلى إتمام التمارين على منظومة تسليم التمارين. وتذكر أن تسلم تمارين القسم بالكامل دفعة واحدة , فلو سلمت تمارين قسمٍ ما، لن تكون قادررًا على تسليم غيرها من القسم ذاته. تشكل بعض التمارين جزءًا من التطبيق ذاته، يكفي في هذه الحالة رفع النسخة النهائية من التطبيق. يمكنك إن أردت الإشارة إلى ذلك بتعليق في نهاية كل تمرين، لكنه أمر غير ملزم.

تحذير تنشئ الأداة create-react-app مستودع git محلي يحتوي المشروع ، إلا إن كان في المجلد مستودع محلي سابق. من المرجح أنك لا تريد أن يغدو المشروع مستودعًا، لهذا نفذ الأمر التاليrm -rf .git في مسار المشروع.

عليك أحيانًا تنفيذ الأمر التالي في نفس المكان:

rm -rf node_modules/ && npm i

1.6 unicafe: الخطوة 1

تجمع Unicafe كمعظم الشركات آراء عملائها. ستكون مهمتك إنشاء تطبيق لجمع آراء العملاء. حيث يقدم التطبيق ثلاث خيارات هي جيد وعادي وسيء. يجب أن يُظهر التطبيق العدد الكلي للآراء في كل فئة. يمكن لتطبيقك أن يكون بالشكل التالي:

unicafe_step1_09.png

ينبغي لتطبيقك العمل خلال جلسة واحدة للمتصفح، وبالتالي لا بأس إن اختفت المعلومات عند إعادة تحميل الصفحة. يمكن وضع الشيفرة في ملف index.js واحد، كما يمكن الاستفادة من الشيفرة التالية كنقطة للبدء:

import React, { useState } from 'react'
import ReactDOM from 'react-dom'

const App = () => {
  // save clicks of each button to own state
  const [good, setGood] = useState(0)
  const [neutral, setNeutral] = useState(0)
  const [bad, setBad] = useState(0)

  return (
    <div>
      code here
    </div>
  )
}

ReactDOM.render(<App />, 
  document.getElementById('root')
)

1.8 unicafe: الخطوة 2

وسّع تطبيقك ليضم إحصائيات أخرى حول آراء العملاء كالعدد الكلي للآراء و التقييم الوسطي (1 للجيد و 0 للعادي و -1 للسيء) والنسبة المئوية للآراء الإيجابية.

unicafe_step1_10.png

1.9 unicafe: الخطوة 3

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

// a proper place to define a component
const Statistics = (props) => {
  // ...
}

const App = () => {
  const [good, setGood] = useState(0)
  const [neutral, setNeutral] = useState(0)
  const [bad, setBad] = useState(0)

  // do not define a component within another component
  const Statistics = (props) => {
    // ...
  }

  return (
    // ...
  )
}

1.9 unicafe: الخطوة 4

غيّر تطبيقك لتعرض الإحصائيات حالما نحصل على رأي المستخدم.

unicafe_step1_11.png

1.10 unicafe: الخطوة 5

سنكمل إعادة صياغة التطبيق. إفصل المكوّنين التاليين ليقوما بالتالي:

  • المكوّن Button سيُعرِّف الأزرار التي تُستخدم للحصول على الرأي.
  • المكوّن Statistic ليظهر إحصائية واحدة مثل متوسط التقييم.

لنوضح الأمر أكثر: يظهر المكوّن Statistic دائمًا إحصائية واحدة، بمعنى أن التطبيق يستخدم مكوّنات متعددة ليصيّر كل الإحصائيات كما يظهر في الشيفرة التالية:

const Statistics = (props) => {
  /// ...
  return(
    <div>
      <Statistic text="good" value ={...} />
      <Statistic text="neutral" value ={...} />
      <Statistic text="bad" value ={...} />
      // ...
    </div>
  )
}

يجب أن تُبق حالة التطبيق ضمن المكوّن App.

1.11 unicafe: الخطوة 6 *

اعرض الإحصائيات في جدول HTML ليبدو تطبيقك مماثلًا بشكل ما للصفحة التالية:

unicafe_step1_12.png

تذكر أن تبقي الطرفية مفتوحةً دائمًا، وإن رأيت هذا الخطأ:

unicafe_step1_13.png

حاول قدر استطاعتك التخلص من التحذيرات التي ستظهر لك. حاول البحث في Google عن رسائل الخطأ التي تصادفك إن لم تتمكن من إيجاد الحل. فمثلًا، المصدر النموذجي للخطأ:

Unchecked runtime.lastError: Could not establish connection. Receiving end does not exist

هو موسّع للمتصفح Chrome. ادخل إلى /chrome://extensions ثم الغ تفعيل الموسّعات واحدًا تلو الآخر حتى تقف على الموسّع الذي سبب الخطأ، ثم أعد تحميل الصفحة. من المفترض أن يصحح ذلك الخطأ.

احرص من الآن فصاعدًا أن لا ترى أية تحذيرات على متصفحك

1.12 طرائف: خطوة 1 *

يمتلئ عالم هندسة البرمجيات بالطرائف التي تختصر الحقائق الخالدة في هذا المجال في سطر واحد قصير. وسّع التطبيق التالي بإضافة زر يعرض بالنقر عليه طرفة مختارة عشوائيًا من مجال هندسة البرمجيات:

import React, { useState } from 'react'
import ReactDOM from 'react-dom'

const App = (props) => {
  const [selected, setSelected] = useState(0)

  return (
    <div>
      {props.anecdotes[selected]}
    </div>
  )
}

const anecdotes = [
  'If it hurts, do it more often',
  'Adding manpower to a late software project makes it later!',
  'The first 90 percent of the code accounts for the first 90 percent of the development time...The remaining 10 percent of the code accounts for the other 90 percent of the development time.',
  'Any fool can write code that a computer can understand. Good programmers write code that humans can understand.',
  'Premature optimization is the root of all evil.',
  'Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.'
]

ReactDOM.render(
  <App anecdotes={anecdotes} />,
  document.getElementById('root')
)

ابحث في Google عن طريقة توليد الأرقام العشوائية في JavaScript. ولا تنس أن تجرب توليد الأرقام العشوائية بعرضها مباشرة على طرفيّة المتصفح. سيبدو التطبيق عندما ينتهي قريبًا من الشكل التالي:

anecdotes_step1_14.png

تحذير تنشئ الأداة create-react-app مستودع git محلي يحتوي المشروع ، إلا إن كان في المجلد مستودع محلي سابق. من المرجح أنك لا تريد أن يغدو المشروع مستودعًا، لهذا نفذ الأمر التاليrm -rf .git في مسار المشروع.

1.13 طرائف: خطوة 2 *

وسّع تطبيقك بحيث يمكنك التصويت لصالح الطرفة المعروضة.

anecdotes_step1_15.png

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

const points = { 0: 1, 1: 3, 2: 4, 3: 2 }

const copy = { ...points }
// زيادة قيمة الخاصية 2 بمقدار 1   
copy[2] += 1     

أو نسخ مصفوفة على النحو:

const points = [1, 4, 6, 3]

const copy = [...points]
// زيادة القيمة في الموقع 2 من المصفوفة بمقدار 1
copy[2] += 1     

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

1.14 طرائف: الخطوة 3*

اجعل النسخة النهائية للتطبيق قادرة على عرض الطرفة التي تحقق أعلى عدد من الأصوات:

anecdotes_step1_16.png

يكفي أن تعرض إحدى الطرائف التي تحقق نفس العدد من الأصوات.

لقد وصلنا إلى نهاية تمارين القسم 1 من المنهاج وحان الوقت لتسليم الحلول إلى GitHub. لا تنسى تحديد التمارين التي سلمتها في منظومة تسليم التمارين.

ترجمة -وبتصرف- للفصل JavaScript من سلسلة Deep Dive Into Modern Web Development

تم التعديل في بواسطة جميل بيلوني


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...