{"id":"l-rn-3-3","courseId":"react-native-apps","lang":"ar","kind":"lesson","summary":"شارك قائمة الرحلات في Trailhead بين الـ tabs والشاشات باستخدام reducer وcontext يُوفَّران من الـ root layout، واشتقّ المجاميع بدلًا من تخزينها، واعرف متى تستحق مكتبة لإدارة الـ state مكانها.","html":"<p>أربعة أجزاء من Trailhead تهتم الآن بالبيانات نفسها. tab السجلّ يعرض الرحلات، وtab الإحصاءات يجمعها، وشاشة التفاصيل تعرض واحدة منها، ونموذج إضافة رحلة (في الدرس التالي) ينشئها. إن حمّلت كل شاشة نسختها الخاصة واحتفظت بها، فستتباعد النسخ: تحذف رحلة في شاشة التفاصيل، وتعود، فتجد tab الإحصاءات ما زال يحسبها، لأن شاشات الـ stack والـ tabs التي زرتها تبقى مركّبة ولا تُعيد التحميل.</p>\n<p>الحل هو مبدأ React الذي تعرفه أصلًا، مطبّقًا على شجرة تنقّل: مصدر واحد للحقيقة، محفوظ فوق كل ما يحتاج إليه.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">أين تعيش الـ state المشتركة في Expo Router</h2>\n<p>في تطبيق ويب كنت ستضع الـ provider قرب الـ component الجذري. في Expo Router، الـ component الجذري هو <code dir=\"ltr\">src/app/_layout.tsx</code>: فهو يرسم الـ Stack، الذي يرسم مجموعة الـ tabs وكل شاشة تُضاف. والـ provider الموضوع حول ذلك الـ Stack يغطّي التطبيق كله.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 260\" role=\"img\" aria-labelledby=\"l-rn-3-3-t1\"><defs><marker id=\"arrow-l-rn-3-3-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-rn-3-3-t1\">يغلّف HikesProvider الـ Stack الجذري في src/app/_layout.tsx؛ ويقرأ tab السجلّ وtab الإحصاءات وشاشة تفاصيل الرحلة الرحلات نفسها ويرسلون actions إلى الـ reducer نفسه.</title>\n  <rect class=\"d-box-primary\" x=\"200\" y=\"16\" width=\"280\" height=\"64\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"340\" y=\"44\" text-anchor=\"middle\">HikesProvider</text>\n  <text class=\"d-code\" x=\"340\" y=\"66\" text-anchor=\"middle\">src/app/_layout.tsx</text>\n  <rect class=\"d-box\" x=\"240\" y=\"104\" width=\"200\" height=\"40\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"340\" y=\"129\" text-anchor=\"middle\">الـ Stack الجذري</text>\n  <path class=\"d-line\" d=\"M340 80 L340 104\"/>\n  <rect class=\"d-box-accent\" x=\"40\" y=\"186\" width=\"170\" height=\"50\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"125\" y=\"216\" text-anchor=\"middle\">tab السجلّ</text>\n  <rect class=\"d-box-accent\" x=\"255\" y=\"186\" width=\"170\" height=\"50\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"340\" y=\"216\" text-anchor=\"middle\">tab الإحصاءات</text>\n  <rect class=\"d-box-accent\" x=\"470\" y=\"186\" width=\"170\" height=\"50\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"555\" y=\"216\" text-anchor=\"middle\">/hikes/[id]</text>\n  <path class=\"d-line\" d=\"M300 144 L125 186\"/>\n  <path class=\"d-line\" d=\"M340 144 L340 186\"/>\n  <path class=\"d-line\" d=\"M380 144 L555 186\"/>\n  <path class=\"d-arrow d-dashed\" d=\"M600 186 C640 120 560 60 482 52\" marker-end=\"url(#arrow-l-rn-3-3-1)\"/>\n  <text class=\"d-label-muted\" x=\"612\" y=\"110\" text-anchor=\"middle\">dispatch</text>\n</svg><figcaption>provider واحد في الـ root layout يغذّي كل شاشة</figcaption></figure>\n<h2 id=\"sec-2\" tabindex=\"-1\">reducer لكل تغيير</h2>\n<p>الرحلات تُضاف وتُعدَّل وتُحذف وتُفضَّل. بدلًا من بعثرة استدعاءات <code dir=\"ltr\">setHikes</code> في الشاشات، صِف كل تغيير بوصفه <strong>action</strong> وعالجها كلها في دالة نقية واحدة:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">src/state/hikesReducer.ts</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Hike</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">date</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">distanceKm</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">favorite</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> boolean</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> HikesAction</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  |</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">type</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'added'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">hike</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Hike</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  |</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">type</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'updated'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">changes</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Partial</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Omit</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Hike</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'id'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>> }</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  |</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">type</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'deleted'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  |</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">type</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'favoriteToggled'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> hikesReducer</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">state</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Hike</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[], </span><span style=\"color:#953800;--shiki-dark:#FFA657\">action</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> HikesAction</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Hike</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[] {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  switch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (action.type) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    case</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'added'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">      return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [action.hike, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">state];</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    case</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'updated'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">      return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> state.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">map</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">h</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (h.id </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> action.id </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">h, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">action.changes } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> h));</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    case</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'deleted'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">      return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> state.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">filter</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">h</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> h.id </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!==</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> action.id);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    case</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'favoriteToggled'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">      return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> state.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">map</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">h</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (h.id </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> action.id </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">h, favorite: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">h.favorite } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> h));</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>كل حالة تُرجع مصفوفة <strong>جديدة</strong>، وكائنًا جديدًا للرحلة التي تغيّرت. هكذا تعرف React أن شيئًا تغيّر، وهذا ما يسمح لـ <code dir=\"ltr\">HikeRow</code> المغلّف بـ memo بتخطّي إعادة الـ render للصفوف الـ 599 التي لم تتغيّر.</p>\n<p>لاحظ أسماء الـ actions: <code dir=\"ltr\">added</code> و<code dir=\"ltr\">deleted</code> و<code dir=\"ltr\">favoriteToggled</code>. إنها تصف <strong>ما حدث</strong>، لا كيفية تحديث الـ state. وهذا يُبقي الشاشات بسيطة (شاشة التفاصيل تُبلغ أن المستخدم بدّل حالة التفضيل، ولا يهمّها كيف تُخزَّن القائمة)، ويعطيك مكانًا واحدًا تغيّره حين تتغيّر القواعد، كأن يصبح حذف رحلة يعني حذف صورها أيضًا.</p>\n<p>ولأن الـ reducer لا يستورد شيئًا من React Native، فهو يعمل في أي مكان. ستختبر reducers كهذا باستخدام Jest العادي في القسم 5، وستكتب واحدًا في تمرين هذا الدرس.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">الـ provider والـ hooks الخاصة به</h2>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">src/state/hikes.tsx</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { createContext, use, useMemo, useReducer, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Dispatch, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ReactNode } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'react'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { hikesReducer, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Hike, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> HikesAction } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> './hikesReducer'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> HikesContext</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> createContext</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Hike</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">|</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> DispatchContext</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> createContext</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Dispatch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">HikesAction</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">> </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">|</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> HikesProvider</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ </span><span style=\"color:#953800;--shiki-dark:#FFA657\">initial</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">children</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">initial</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Hike</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[]; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">children</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> ReactNode</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">hikes</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">dispatch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useReducer</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(hikesReducer, initial);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">DispatchContext</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> value</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">dispatch</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">HikesContext</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> value</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">hikes</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">children</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">HikesContext</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    &#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">DispatchContext</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  );</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useHikes</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> hikes</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> use</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(HikesContext);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">hikes) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">throw</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Error</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'useHikes must be used inside HikesProvider'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> hikes;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useHike</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> undefined</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> hikes</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useHikes</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useMemo</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> hikes.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">find</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">h</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> h.id </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> id), [hikes, id]);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useHikesDispatch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> dispatch</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> use</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(DispatchContext);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">dispatch) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">throw</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Error</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'useHikesDispatch must be used inside HikesProvider'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> dispatch;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>ثم غلّف الـ Stack في الـ root layout: <code dir=\"ltr\">&lt;HikesProvider initial={sampleHikes}&gt;&lt;Stack&gt;…&lt;/Stack&gt;&lt;/HikesProvider&gt;</code>. وفي القسم 4 ستأتي البيانات الأولية من قاعدة بيانات الجهاز بدلًا من ذلك.</p>\n<p>بعض التفاصيل تستحق النسخ. تسمح لك React 19 برسم الـ context مباشرة بوصفه provider (<code dir=\"ltr\">&lt;HikesContext value={…}&gt;</code>) وقراءته باستخدام <code dir=\"ltr\">use</code>. وفصل الـ state والـ dispatch في <strong>اثنين من الـ contexts</strong> منفصلَين يعني أن الـ components التي ترسل actions فقط، مثل زرّ حذف، لا يُعاد لها الـ render حين تتغيّر القائمة. والـ hooks ترمي خطأً واضحًا إن نسي أحدٌ الـ provider، بدلًا من الفشل لاحقًا برسالة “cannot read properties of null”.</p>\n<p>وهكذا تُقرأ الشاشة الآن:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">TSX</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> hike</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useHike</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(id);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> dispatch</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useHikesDispatch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// …</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Pressable</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> onPress</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> dispatch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ type: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'favoriteToggled'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, id: hike.id })</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n<h2 id=\"sec-4\" tabindex=\"-1\">اشتقّ ولا تكرّر</h2>\n<p>يحتاج tab الإحصاءات إلى المسافة الإجمالية، وعدد الرحلات، وأطول رحلة. لا شيء من ذلك state. إنها قيم <strong>مشتقّة</strong> من قائمة الرحلات، فاحسبها:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">TSX</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> hikes</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useHikes</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> stats</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useMemo</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> totalKm</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> hikes.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">reduce</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">sum</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">h</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sum </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> h.distanceKm, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> longest</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> hikes.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">reduce</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">best</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">h</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">best </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">||</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> h.distanceKm </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> best.distanceKm </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> h </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> best), </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">undefined</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> as</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Hike</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> undefined</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { count: hikes.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">length</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, totalKm, longest };</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}, [hikes]);</span></span></code></pre></div>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">خطأ شائع</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>نسخ البيانات المشتركة إلى state محلية</span></p>\n<p><code dir=\"ltr\">const [hike, setHike] = useState(useHike(id))</code> يلتقط صورة للرحلة لحظة تركيب الشاشة. عدّلها من مكان آخر وستبقى هذه الشاشة تعرض النسخة القديمة، لأن <code dir=\"ltr\">useState</code> لا تستخدم وسيطها إلا في أول render. اقرأ من الـ hook المشترك في كل render؛ وانسخ إلى state محلية فقط لمسوّدة يعدّلها المستخدم، كالنموذج، ثم أرسل action حين يحفظ.</p>\n</aside>\n<h2 id=\"sec-5\" tabindex=\"-1\">حين لا يكفي الـ context</h2>\n<p>يعيد الـ context الـ render لكل مستهلك حين تتغيّر قيمته. لقائمة الرحلات في Trailhead هذا جيد. لكنه يصبح مشكلة حين تتغيّر قيمة ما كثيرًا جدًا (موقع GPS حيّ يتحدّث كل ثانية)، أو حين يكون في تطبيق كبير مستهلكون كثيرون لا علاقة بينهم. عندها تلجأ الفرق إلى <strong>Zustand</strong>، وهي store صغيرة مع selectors بحيث لا يُعاد الـ render للـ components إلا بسبب الشريحة التي تقرؤها. أما البيانات التي تعيش على خادم فلها مشكلات مختلفة تمامًا (التخزين المؤقت، وإعادة الجلب، وتقادم البيانات)، وهذه وظيفة TanStack Query؛ ستلتقي بها في القسم 4. ابدأ بالـ reducer مع الـ context، وانتقل حين تقيس سببًا لذلك.</p>\n<p>التالي: الشاشة التي تنشئ الرحلات، أي نموذج يعمل مع لوحة مفاتيح الهاتف.</p>\n","toc":[{"id":"sec-1","text":"أين تعيش الـ state المشتركة في Expo Router","level":2},{"id":"sec-2","text":"reducer لكل تغيير","level":2},{"id":"sec-3","text":"الـ provider والـ hooks الخاصة به","level":2},{"id":"sec-4","text":"اشتقّ ولا تكرّر","level":2},{"id":"sec-5","text":"حين لا يكفي الـ context","level":2}],"takeaways":["الـ state التي تستخدمها عدّة مسارات مكانها فوقها جميعًا، وهذا في Expo Router يعني provider داخل <code dir=\"ltr\">_layout.tsx</code> الجذري.","يجمع الـ reducer كل تغيير على قائمة الرحلات في دالة نقية واحدة قابلة للاختبار تُرجع مصفوفات وكائنات جديدة.","غلّف الـ context بـ hooks صغيرة مثل <code dir=\"ltr\">useHikes()</code> و<code dir=\"ltr\">useHike(id)</code>، حتى لا تلمس الشاشات كائن الـ context مباشرة أبدًا.","اشتقّ المجاميع والقوائم المصفّاة من الـ state المصدر باستخدام <code dir=\"ltr\">useMemo</code>؛ ولا تخزّنها منفصلة.","استخدم مكتبة مثل Zustand حين تصبح عمليات re-render الناتجة عن الـ context مشكلة مقيسة، وTanStack Query حين تعيش البيانات على خادم."],"further":[{"title":"Scaling Up with Reducer and Context","url":"https://react.dev/learn/scaling-up-with-reducer-and-context"},{"title":"Choosing the State Structure","url":"https://react.dev/learn/choosing-the-state-structure"},{"title":"useReducer","url":"https://react.dev/reference/react/useReducer"}],"quiz":[{"q":"<p>يخزّن tab الإحصاءات <code dir=\"ltr\">totalKm</code> في <code dir=\"ltr\">useState</code> خاصة به، تُحدَّث داخل effect. بعد أن يحذف المستخدم رحلة، يبقى المجموع خاطئًا حتى يُعاد تشغيل التطبيق. ما أفضل إصلاح؟</p>","options":[{"text":"أضف مصفوفة الرحلات إلى قائمة اعتماديات الـ effect.","why":"هذا يرقّع العَرَض، لكن تبقى لديك نسختان من الحقيقة وrender إضافي مع كل تغيير."},{"text":"انقل <code dir=\"ltr\">totalKm</code> إلى الـ reducer وحدّثه مع كل action.","why":"الآن يجب على كل action أن يتذكّر إبقاء المجموع متزامنًا، ومن هنا يأتي هذا النوع من الأخطاء بالضبط."},{"text":"أعِد تركيب tab الإحصاءات كلما حصل على التركيز.","why":"رمي الـ state الخاصة بالشاشة للتعافي من خطأ تزامن يكلّف أكثر من الخطأ نفسه."},{"text":"احذف الـ state واحسب المجموع من الرحلات المشتركة باستخدام <code dir=\"ltr\">useMemo</code>.","why":"صحيح. القيم المشتقّة المحسوبة من مصدر واحد للحقيقة لا يمكن أن تخرج عن التزامن."}],"answer":3},{"q":"<p>ما المشكلة في حالة الـ reducer هذه؟</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">JavaScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">case</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'favoriteToggled'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  const hike </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> state.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">find</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">h</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> h.id </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> action.id);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  hike.favorite </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> !</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">hike.favorite;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  return state;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n","options":[{"text":"تعدّل الرحلة الموجودة وتُرجع المصفوفة نفسها، فلا ترى React أي تغيير ولا يحدث re-render.","why":"صحيح. أرجع مصفوفة جديدة فيها كائن جديد للرحلة التي تغيّرت، باستخدام <code dir=\"ltr\">state.map</code> مثلًا."},{"text":"لا يمكن أن تحتوي الـ reducers على تعريفات <code dir=\"ltr\">const</code> محصورة في كتلة.","why":"التعريفات المحصورة في كتلة داخل <code dir=\"ltr\">case</code> بين أقواس معقوفة لا بأس بها."},{"text":"<code dir=\"ltr\">find</code> أبطأ من أن يُستخدم في reducer؛ يحتاج إلى بحث عبر فهرس.","why":"مع مئات الرحلات يكون <code dir=\"ltr\">find</code> فوريًا. الخطأ في التعديل المباشر (mutation)، لا في السرعة."}],"answer":0},{"q":"<p>أين يجب أن يُرسم <code dir=\"ltr\">HikesProvider</code> الخاص بـ Trailhead لكي يقرأه tab السجلّ وtab الإحصاءات وشاشة <code dir=\"ltr\">/hikes/[id]</code> جميعًا؟</p>","options":[{"text":"داخل <code dir=\"ltr\">(tabs)/index.tsx</code>، لأن السجلّ هو مالك الرحلات.","why":"الإخوة وشاشة التفاصيل خارج ذلك الـ component، فلا يستطيعون قراءة الـ context الخاص به."},{"text":"في <code dir=\"ltr\">src/app/_layout.tsx</code>، حول الـ Stack الجذري.","why":"صحيح. كل ما يرسمه الـ root layout، الـ tabs وشاشات الـ stack على السواء، يقع داخل الـ provider."},{"text":"في كل شاشة تحتاج إليه، ولكلٍّ منها نسختها الخاصة.","why":"الـ providers المنفصلة تعني state منفصلة؛ فالتعديل في شاشة لن يظهر في الأخرى."}],"answer":1}],"exercise":{"kind":"web","mode":"js","starter":{"js":"// Trailhead keeps hikes sorted newest first (dates are 'YYYY-MM-DD').\nfunction hikesReducer(state, action) {\n  switch (action.type) {\n    case 'added':\n      state.push(action.hike);\n      return state;\n    case 'updated': {\n      const hike = state.find((h) => h.id === action.id);\n      Object.assign(hike, action.changes);\n      return state;\n    }\n    case 'deleted':\n      return state.filter((h) => h.id !== action.id);\n    case 'favoriteToggled': {\n      const hike = state.find((h) => h.id === action.id);\n      hike.favorite = !hike.favorite;\n      return state;\n    }\n    default:\n      throw new Error('Unknown action ' + action.type);\n  }\n}\n\nconst start = [\n  { id: 'a', name: 'Lake Trail', date: '2026-10-19', distanceKm: 11.2, favorite: false },\n  { id: 'b', name: 'Ridge Loop', date: '2026-10-04', distanceKm: 8.4, favorite: true },\n];\nconsole.log(hikesReducer(start, { type: 'added', hike: { id: 'c', name: 'Fern Gully', date: '2026-10-10', distanceKm: 3.6, favorite: false } }).map((h) => h.id));\n"},"solution":{"js":"const byNewest = (a, b) => b.date.localeCompare(a.date);\n\nfunction hikesReducer(state, action) {\n  switch (action.type) {\n    case 'added':\n      if (state.some((h) => h.id === action.hike.id)) return state;\n      return [...state, action.hike].sort(byNewest);\n    case 'updated': {\n      if (!state.some((h) => h.id === action.id)) return state;\n      return state\n        .map((h) => (h.id === action.id ? { ...h, ...action.changes, id: h.id } : h))\n        .sort(byNewest);\n    }\n    case 'deleted':\n      return state.filter((h) => h.id !== action.id);\n    case 'favoriteToggled':\n      return state.map((h) => (h.id === action.id ? { ...h, favorite: !h.favorite } : h));\n    default:\n      return state;\n  }\n}\n\nconst start = [\n  { id: 'a', name: 'Lake Trail', date: '2026-10-19', distanceKm: 11.2, favorite: false },\n  { id: 'b', name: 'Ridge Loop', date: '2026-10-04', distanceKm: 8.4, favorite: true },\n];\nconsole.log(hikesReducer(start, { type: 'added', hike: { id: 'c', name: 'Fern Gully', date: '2026-10-10', distanceKm: 3.6, favorite: false } }).map((h) => h.id));\n"},"checks":[{"id":"add-sorted","test":"const mk = () => [{ id: 'a', name: 'Lake Trail', date: '2026-10-19', distanceKm: 11.2, favorite: false }, { id: 'b', name: 'Ridge Loop', date: '2026-10-04', distanceKm: 8.4, favorite: true }]; const s = mk(); const n = hikesReducer(s, { type: 'added', hike: { id: 'c', name: 'Fern', date: '2026-10-10', distanceKm: 3.6, favorite: false } }); return n !== s && n.map((h) => h.id).join() === 'a,c,b' && s.length === 2;"},{"id":"add-duplicate","test":"const mk = () => [{ id: 'a', name: 'Lake Trail', date: '2026-10-19', distanceKm: 11.2, favorite: false }, { id: 'b', name: 'Ridge Loop', date: '2026-10-04', distanceKm: 8.4, favorite: true }]; const s = mk(); const n = hikesReducer(s, { type: 'added', hike: { id: 'a', name: 'Copy', date: '2026-01-01', distanceKm: 1, favorite: false } }); return n === s;"},{"id":"update","test":"const mk = () => [{ id: 'a', name: 'Lake Trail', date: '2026-10-19', distanceKm: 11.2, favorite: false }, { id: 'b', name: 'Ridge Loop', date: '2026-10-04', distanceKm: 8.4, favorite: true }]; const s = mk(); const n = hikesReducer(s, { type: 'updated', id: 'b', changes: { name: 'Ridge Loop East', date: '2026-10-25', id: 'zzz' } }); return n !== s && n[0].id === 'b' && n[0].name === 'Ridge Loop East' && s[1].name === 'Ridge Loop' && n.length === 2;"},{"id":"toggle","test":"const mk = () => [{ id: 'a', name: 'Lake Trail', date: '2026-10-19', distanceKm: 11.2, favorite: false }, { id: 'b', name: 'Ridge Loop', date: '2026-10-04', distanceKm: 8.4, favorite: true }]; const s = mk(); const n = hikesReducer(s, { type: 'favoriteToggled', id: 'a' }); return n !== s && n[0].favorite === true && s[0].favorite === false && n[1] === s[1];"},{"id":"delete","test":"const mk = () => [{ id: 'a', name: 'Lake Trail', date: '2026-10-19', distanceKm: 11.2, favorite: false }, { id: 'b', name: 'Ridge Loop', date: '2026-10-04', distanceKm: 8.4, favorite: true }]; const s = mk(); const n = hikesReducer(s, { type: 'deleted', id: 'a' }); return n.length === 1 && n[0].id === 'b' && s.length === 2;"},{"id":"unknown","test":"const mk = () => [{ id: 'a', name: 'Lake Trail', date: '2026-10-19', distanceKm: 11.2, favorite: false }, { id: 'b', name: 'Ridge Loop', date: '2026-10-04', distanceKm: 8.4, favorite: true }]; const s = mk(); let n; try { n = hikesReducer(s, { type: 'somethingElse' }); } catch { return false; } return n === s;"}],"title":"اجعل reducer الرحلات غير قابل للتعديل المباشر","prompt":"<p>يعدّل <code dir=\"ltr\">hikesReducer</code> في Trailhead الـ state مباشرة، فلا تتحدّث الشاشات. أعد كتابته بحيث تُرجع <strong>كل</strong> حالة مصفوفات وكائنات جديدة ولا تغيّر <code dir=\"ltr\">state</code> أبدًا. أبقِ القائمة مرتّبة <strong>من الأحدث أولًا</strong> بحسب <code dir=\"ltr\">date</code> (نصوص بصيغة <code dir=\"ltr\">YYYY-MM-DD</code>).</p>\n<ul>\n<li><code dir=\"ltr\">added</code>: أدرج <code dir=\"ltr\">action.hike</code> بترتيب التاريخ. إن كانت رحلة بذلك الـ <code dir=\"ltr\">id</code> موجودة أصلًا، فأرجع <code dir=\"ltr\">state</code> <strong>نفسها</strong>.</li>\n<li><code dir=\"ltr\">updated</code>: ادمج <code dir=\"ltr\">action.changes</code> في الرحلة ذات <code dir=\"ltr\">action.id</code>، وأعد الترتيب، ولا تسمح أبدًا لـ <code dir=\"ltr\">changes</code> بالكتابة فوق الـ <code dir=\"ltr\">id</code>.</li>\n<li><code dir=\"ltr\">deleted</code>: احذف الرحلة.</li>\n<li><code dir=\"ltr\">favoriteToggled</code>: اقلب قيمة <code dir=\"ltr\">favorite</code>؛ والرحلات التي لم تتغيّر تحتفظ <strong>بالكائن نفسه</strong>، لكي تتخطّى الصفوف المغلّفة بـ memo إعادة الـ render.</li>\n<li>أي action غير معروف يُرجع <code dir=\"ltr\">state</code> نفسها (لا ترمِ خطأ).</li>\n</ul>\n","hints":["أنشئ دالة مقارنة مرة واحدة: <code dir=\"ltr\">const byNewest = (a, b) =&gt; b.date.localeCompare(a.date);</code>. و<code dir=\"ltr\">[...state, hike].sort(byNewest)</code> يرتّب نسخة.","<code dir=\"ltr\">state.map((h) =&gt; (h.id === action.id ? { ...h, ...changes } : h))</code> ينسخ الرحلة التي تغيّرت فقط. ضع <code dir=\"ltr\">id: h.id</code> بعد الـ spread لتكون لها الغلبة.","إرجاع <code dir=\"ltr\">state</code> نفسها (المرجع نفسه) هو الطريقة التي يخبر بها الـ reducer React أن شيئًا لم يتغيّر."],"explanation":"<p>تقرّر React إعادة الـ render بمقارنة المراجع، لذا فإن reducer يعدّل مباشرة ويُرجع المصفوفة نفسها يُخفي كل تغيير. تبني النسخة المُصلحة مصفوفات جديدة باستخدام الـ spread و<code dir=\"ltr\">map</code> و<code dir=\"ltr\">filter</code> وترتيب على نسخة، وتنشئ كائنًا جديدًا فقط للرحلة التي تغيّرت، وتُرجع <code dir=\"ltr\">state</code> الأصلية حين لا يجب أن يتغيّر شيء (إضافة مكرّرة أو action غير معروف). الرحلات التي لم تتغيّر تحتفظ بهويّتها، وهذا بالضبط ما يتيح لـ <code dir=\"ltr\">memo(HikeRow)</code> تخطّيها.</p>\n","checkLabels":{"add-sorted":"الرحلات المضافة تقع بترتيب التاريخ في مصفوفة جديدة","add-duplicate":"إضافة معرّف موجود تُرجع الـ state نفسها","update":"التحديث يدمج ويعيد الترتيب ويحتفظ بالمعرّف ولا يعدّل مباشرة","toggle":"تبديل التفضيل ينسخ الرحلة التي تغيّرت فقط","delete":"الحذف يُرجع مصفوفة جديدة أقصر","unknown":"الـ actions غير المعروفة تُرجع الـ state نفسها دون رمي خطأ"}},"words":593,"readMinutes":3}