{"id":"l-rn-4-1","courseId":"react-native-apps","lang":"ar","kind":"lesson","summary":"حمّل كتالوج الدروب في Trailhead من خادم باستخدام fetch، وصمّم حالات التحميل والخطأ والفراغ والنجاح صراحةً، وألغِ الطلبات القديمة، وتعامل مع الشبكات المتقلّبة التي تعيش عليها الهواتف.","html":"<p>حتى الآن، عاشت بيانات Trailhead على الهاتف. أما الميزة التالية، كتالوج الدروب القريبة منك، فتأتي من خادم. على حاسوب محمول متصل بشبكة Wi-Fi المكتب، الطلب مجرّد إجراء شكلي. أما على هاتف عند بداية درب وفيه خطّ إشارة واحد، فهو أبطأ ما يفعله تطبيقك وأقلّه موثوقية: تستغرق الطلبات ثواني، أو تفشل في منتصفها، أو لا تعود أبدًا. والتصميم لهذا الواقع هو معظم العمل.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">أربع حالات، وقيمة واحدة</h2>\n<p>كل شاشة تحمّل بيانات لها أربع حالات على الأقل يمكن أن يراها المستخدم: <strong>التحميل</strong> (loading)، و<strong>الخطأ</strong> (error)، و<strong>الفراغ</strong> (empty: اكتمل التحميل ولا يوجد شيء)، و<strong>النجاح</strong> (success). مثّلها بقيمة واحدة، فلا يمكن أن تحدث تركيبات مستحيلة:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">src/lib/types.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\"> Trail</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\">lengthKm</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\">difficulty</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'easy'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'moderate'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'hard'</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\"> Load</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">T</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">> </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\">status</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'loading'</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\">status</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'error'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">message</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\">status</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'success'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">data</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> T</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span></code></pre></div>\n<p>الفراغ ليس حالة منفصلة؛ إنه نجاح بصفر عناصر، والشاشة تقرّر كيف تعرضه.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 660 230\" role=\"img\" aria-labelledby=\"l-rn-4-1-t1\"><defs><marker id=\"arrow-l-rn-4-1-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-4-1-t1\">تبدأ الشاشة في حالة loading. الاستجابة الناجحة تنقلها إلى success، التي تعرض القائمة أو حالة الفراغ. والفشل ينقلها إلى error، وزرّ إعادة المحاولة يعيدها إلى loading. والسحب للتحديث ينقلها من success إلى loading من جديد.</title>\n  <rect class=\"d-box-accent\" x=\"250\" y=\"20\" width=\"160\" height=\"54\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"330\" y=\"53\" text-anchor=\"middle\">loading</text>\n  <rect class=\"d-box-success\" x=\"440\" y=\"150\" width=\"190\" height=\"60\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"535\" y=\"176\" text-anchor=\"middle\">success</text>\n  <text class=\"d-label-muted\" x=\"535\" y=\"198\" text-anchor=\"middle\">القائمة أو حالة الفراغ</text>\n  <rect class=\"d-box-warn\" x=\"30\" y=\"150\" width=\"190\" height=\"60\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"125\" y=\"176\" text-anchor=\"middle\">error</text>\n  <text class=\"d-label-muted\" x=\"125\" y=\"198\" text-anchor=\"middle\">رسالة + إعادة المحاولة</text>\n  <path class=\"d-arrow\" d=\"M380 74 L500 150\" marker-end=\"url(#arrow-l-rn-4-1-1)\"/>\n  <text class=\"d-label-muted\" x=\"480\" y=\"104\" text-anchor=\"middle\">استجابة ok</text>\n  <path class=\"d-arrow\" d=\"M280 74 L160 150\" marker-end=\"url(#arrow-l-rn-4-1-1)\"/>\n  <text class=\"d-label-muted\" x=\"180\" y=\"104\" text-anchor=\"middle\">فشل</text>\n  <path class=\"d-arrow d-dashed\" d=\"M220 168 C300 140 280 110 300 76\" marker-end=\"url(#arrow-l-rn-4-1-1)\"/>\n  <text class=\"d-label-muted\" x=\"300\" y=\"150\" text-anchor=\"middle\">إعادة</text>\n  <path class=\"d-arrow d-dashed\" d=\"M440 168 C380 140 380 110 362 76\" marker-end=\"url(#arrow-l-rn-4-1-1)\"/>\n  <text class=\"d-label-muted\" x=\"400\" y=\"150\" text-anchor=\"middle\">تحديث</text>\n</svg><figcaption>الطلب ينتقل بين حالات صريحة</figcaption></figure>\n<h2 id=\"sec-2\" tabindex=\"-1\">الجلب مع التنظيف</h2>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">src/app/(tabs)/explore.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\"> { useCallback, useEffect, useState } </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\"> { ActivityIndicator, FlatList, Pressable, Text, View } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'react-native'</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:#CF222E;--shiki-dark:#FF7B72\"> type</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { Load, Trail } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '../../lib/types'</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\"> API</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> process.env.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">EXPO_PUBLIC_API_URL</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\">async</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> getTrails</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">signal</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> AbortSignal</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Promise</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Trail</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\"> res</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> fetch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`${</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">API</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}/trails?near=current`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, { signal });</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\">res.ok) </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\">`Server responded ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">res</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">status</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</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\"> res.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">json</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>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> default</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Explore</span><span style=\"color:#953800;--shiki-dark:#FFA657\">() </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\">state</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">setState</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useState</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Load</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Trail</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[]>>({ status: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'loading'</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\">attempt</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">setAttempt</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useState</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</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\"> retry</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useCallback</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> setAttempt</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">n</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> n </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">), []);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  useEffect</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\"> controller</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> AbortController</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">    setState</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ status: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'loading'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> });</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">    getTrails</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(controller.signal)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      .</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">then</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">data</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> setState</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ status: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'success'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, data }))</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      .</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">catch</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\">        if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (controller.signal.aborted) </span><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:#8250DF;--shiki-dark:#D2A8FF\">        setState</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ status: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'error'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, message: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Could not load trails. Check your connection and try again.'</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:#CF222E;--shiki-dark:#FF7B72\">    return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> controller.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">abort</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }, [attempt]);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (state.status </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'loading'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">ActivityIndicator</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> style</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ flex: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> size</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"large\"</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\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (state.status </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'error'</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\"> (</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">View</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> style</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ flex: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, justifyContent: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'center'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, alignItems: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'center'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, gap: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">12</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, padding: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">24</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:#1F2328;--shiki-dark:#E6EDF3\">        &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Text</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> style</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ textAlign: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'center'</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\">state.message</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\">Text</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\">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\">retry</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> accessibilityRole</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"button\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Text</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Try again&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Text</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Pressable</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\">View</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\">  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\">FlatList</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">      data</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">state.data</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">      keyExtractor</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">t</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> t.id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">      renderItem</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ </span><span style=\"color:#953800;--shiki-dark:#FFA657\">item</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }) </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\">Text</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> style</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ padding: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">16</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\">item.name</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\">item.lengthKm</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> km&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Text</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">      ListEmptyComponent</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\">Text</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> style</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ padding: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">24</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>No trails within 50 km. Try a wider search.&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Text</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</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 style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>ثلاثة تفاصيل تحمل الثقل. يُفحص <code dir=\"ltr\">res.ok</code> يدويًا، لأن <code dir=\"ltr\">fetch</code> يعامل الـ 500 على أنه توصيل ناجح لأخبار سيئة. ويلغي الـ <code dir=\"ltr\">AbortController</code> الطلب في دالة التنظيف، فمغادرة الشاشة (أو إعادة المحاولة) لا تسمح أبدًا لاستجابة قديمة بالكتابة فوق استجابة حديثة. ورسالة الخطأ تخبر المستخدم بما يفعل، بدلًا من طباعة <code dir=\"ltr\">TypeError: Network request failed</code>.</p>\n<p>يأتي <code dir=\"ltr\">EXPO_PUBLIC_API_URL</code> من ملف <code dir=\"ltr\">.env</code>. المتغيّرات التي تبدأ بـ <code dir=\"ltr\">EXPO_PUBLIC_</code> تُدرج مباشرة في حزمة JavaScript، وهذا يعني أيضًا أنها <strong>قابلة للقراءة من أي أحد</strong> يفكّ حزمة تطبيقك. ضع فيها العناوين، ولا تضع المفاتيح السرّية أبدًا.</p>\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>استدعاء localhost من الهاتف</span></p>\n<p>يعمل <code dir=\"ltr\">fetch('http://localhost:3000/trails')</code> في متصفّح الويب على حاسوبك، ويفشل على الهاتف، لأن <code dir=\"ltr\">localhost</code> هناك هو الهاتف نفسه. استخدم عنوان حاسوبك على الشبكة (<code dir=\"ltr\">http://192.168.1.20:3000</code>) أثناء التطوير؛ أما محاكي Android فيصل إلى الجهاز المضيف عبر <code dir=\"ltr\">10.0.2.2</code>. والمنصّتان تقيّدان كذلك <code dir=\"ltr\">http://</code> العادي في builds الإصدار، لذا يجب أن تكون الـ APIs في بيئة الإنتاج على <code dir=\"ltr\">https://</code>.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">السحب للتحديث والشبكات البطيئة</h2>\n<p>يتوقّع المستخدمون أن يسحبوا القائمة إلى الأسفل لإعادة تحميلها. يدعم FlatList ذلك بخاصّيتين:</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:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">FlatList</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">  // …</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  refreshing</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">refreshing</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  onRefresh</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={async</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:#8250DF;--shiki-dark:#D2A8FF\">    setRefreshing</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">true</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    try</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">setState</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ status: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'success'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, data: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> getTrails</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> AbortController</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">().signal) }); }</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    catch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">/* keep the old list and show a short \"Couldn't refresh\" message */</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    finally</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">setRefreshing</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">false</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">); }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">/></span></span></code></pre></div>\n<p>يُبقي التحديث القائمة القديمة ظاهرة أثناء التحميل، وهذا ألطف من مؤشّر التحميل بملء الشاشة في التحميل الأول. وإن فشل التحديث، فاحتفظ بالبيانات القديمة واعرض رسالة قصيرة، بدلًا من استبدال قائمة سليمة تمامًا بشاشة خطأ.</p>\n<p>والشبكات المتقلّبة حجّة أيضًا لصالح <strong>إعادة المحاولة التلقائية</strong> مع تأخير يتزايد بين المحاولات (ثانية، ثم ثانيتان، ثم أربع)، فيتعافى التطبيق وحده من انقطاع قصير في الإشارة. لا تُعِد المحاولة إلا لما يمكن أن ينجح لاحقًا: انتهاء المهلة أو 503 قد ينجحان، أما 404 أو 401 فلن ينجحا، لذا فالفشل السريع ألطف. ستكتب هذه الدالة المساعدة في التمرين.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">غياب الاتصال حالة طبيعية، لا خطأ</h2>\n<p>في تطبيق للمشي الجبلي، غياب الإشارة هو الوضع المتوقّع لساعات متواصلة. تعامل معه على هذا الأساس. اعرض آخر قائمة حمّلتها، مع شريط هادئ يذكر وقت آخر تحديث، بدلًا من شاشة خطأ. ودع المستخدمين يواصلون تسجيل الرحلات، واحفظها على الجهاز، وأرسلها حين يعود الاتصال. تخبرك حزمة المجتمع <code dir=\"ltr\">@react-native-community/netinfo</code> هل الجهاز متصل، وتُعلمك حين يتغيّر ذلك، وتلك هي اللحظة لإعادة محاولة الأعمال المؤجّلة.</p>\n<p>لا شيء من ذلك يحتاج إلى أن يكون ذكيًا من اليوم الأول. المهم هو القرار: لكل شاشة، ماذا يرى المستخدم دون شبكة؟ اكتب الإجابة قبل أن تكتب الـ fetch، وصمّم حالات الفراغ والخطأ وغياب الاتصال بالعناية نفسها التي تصمّم بها المسار السعيد.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">متى تستخدم مكتبة للبيانات</h2>\n<p>الكود أعلاه جيد لشاشة واحدة. أما في تطبيق حقيقي فستكرّره في كل مكان، وسيبقى ينقصك التخزين المؤقت (عُد إلى شاشة فترى البيانات فورًا)، ومنع تكرار الطلبات، وإعادة الجلب في الخلفية، وإعادة المحاولة. تتولّى <strong>TanStack Query</strong> كل ذلك عبر <code dir=\"ltr\">useQuery</code>، وتتضمّن وثائقها الإعداد الصغير الخاص بـ React Native الذي يعيد الجلب حين يعود التطبيق إلى الواجهة. تعلّم النسخة اليدوية أولًا لتعرف ما تفعله المكتبة، ثم استخدم المكتبة في بيئة الإنتاج.</p>\n<p>التالي: بيانات يجب أن تصمد دون أي شبكة على الإطلاق، مخزّنة على الجهاز.</p>\n","toc":[{"id":"sec-1","text":"أربع حالات، وقيمة واحدة","level":2},{"id":"sec-2","text":"الجلب مع التنظيف","level":2},{"id":"sec-3","text":"السحب للتحديث والشبكات البطيئة","level":2},{"id":"sec-4","text":"غياب الاتصال حالة طبيعية، لا خطأ","level":2},{"id":"sec-5","text":"متى تستخدم مكتبة للبيانات","level":2}],"takeaways":["مثّل الطلب بقيمة حالة واحدة (loading أو error أو success) بدلًا من عدّة قيم منطقية قد يناقض بعضها بعضًا.","<code dir=\"ltr\">fetch</code> لا يرفض (reject) إلا عند فشل الشبكة؛ تحقّق من <code dir=\"ltr\">response.ok</code> بنفسك لاستجابات 404 و500.","ألغِ الطلبات الجارية في دالة التنظيف الخاصة بالـ effect، لأن المستخدمين يغادرون الشاشات قبل أن تنتهي الطلبات البطيئة.","كل قائمة تحمّل بيانات تحتاج إلى أربع حالات مصمّمة، منها حالة فراغ وحالة خطأ مع زرّ لإعادة المحاولة.","على الهاتف، <code dir=\"ltr\">localhost</code> هو الهاتف نفسه؛ وجّه الـ development builds إلى عنوان حاسوبك على الشبكة أو إلى API منشور."],"further":[{"title":"Networking","url":"https://reactnative.dev/docs/network"},{"title":"Using fetch","url":"https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch"},{"title":"RefreshControl","url":"https://reactnative.dev/docs/refreshcontrol"}],"quiz":[{"q":"<p>يُرجع الـ API الخاص بـ Trailhead الاستجابة <code dir=\"ltr\">404 Not Found</code> لدرب محذوف. ماذا يفعل <code dir=\"ltr\">await fetch(url)</code>؟</p>","options":[{"text":"يرمي خطأ، فتتعامل معه كتلة <code dir=\"ltr\">catch</code>.","why":"لا يرفض fetch إلا حين لا تصل أي استجابة على الإطلاق (عدم اتصال، فشل DNS، إلغاء). والـ 404 استجابة."},{"text":"يُحَلّ (resolve) باستجابة قيمة <code dir=\"ltr\">ok</code> فيها <code dir=\"ltr\">false</code> و<code dir=\"ltr\">status</code> تساوي 404، ويجب أن تتحقّق منها.","why":"صحيح. تحقّق بـ <code dir=\"ltr\">if (!res.ok)</code> وارمِ خطأ أو تعامل معه بنفسك."},{"text":"يُحَلّ بالقيمة <code dir=\"ltr\">undefined</code>.","why":"يُحَلّ fetch دائمًا بكائن Response حين يجيب الخادم، أيًّا كان رمز الحالة."}],"answer":1},{"q":"<p>يفتح مستخدم شاشة تفاصيل درب ثم يعود فورًا. ينتهي الطلب بعد ثانيتين ويستدعي <code dir=\"ltr\">setState</code>. ما الذي كان يجب أن يفعله الـ effect؟</p>","options":[{"text":"لا شيء؛ يتجاهل React Native تحديثات الـ state من الشاشات المُزالة، فالأمر غير ضار.","why":"يُسقَط التحديث، لكن الطلب استهلك بيانات وبطارية، ويمكن لاستجابة سابقة بطيئة أن تكتب فوق استجابة أحدث حين يتغيّر المعرّف."},{"text":"غلّف الاستدعاء بـ <code dir=\"ltr\">setTimeout</code> ليعمل بعد التنقّل.","why":"تأخير الطلب لا يمنعه من الانتهاء بعد أن تختفي الشاشة."},{"text":"أنشأ <code dir=\"ltr\">AbortController</code> واستدعى <code dir=\"ltr\">controller.abort()</code> في دالة التنظيف.","why":"صحيح. الإلغاء يوقف الطلب عند إزالة الشاشة أو حين يتغيّر المعرّف، فلا تصل الاستجابات القديمة أبدًا."}],"answer":2},{"q":"<p>أيّ شكل للـ state يناسب شاشة الكتالوج في Trailhead أكثر؟</p>","options":[{"text":"<code dir=\"ltr\">{ status: 'loading' } | { status: 'error'; error: string } | { status: 'success'; trails: Trail[] }</code>","why":"صحيح. لا توجد إلا التركيبات الصالحة، وتجبرك TypeScript على التعامل مع كلٍّ منها."},{"text":"<code dir=\"ltr\">isLoading</code> و<code dir=\"ltr\">hasError</code> و<code dir=\"ltr\">trails</code> في ثلاثة استدعاءات <code dir=\"ltr\">useState</code> منفصلة.","why":"لا شيء يمنع أن تكون <code dir=\"ltr\">isLoading</code> و<code dir=\"ltr\">hasError</code> صحيحتين معًا، أو أن تظهر دروب قديمة بجوار خطأ."},{"text":"<code dir=\"ltr\">trails: Trail[] | null</code>، حيث <code dir=\"ltr\">null</code> تعني التحميل أو الخطأ.","why":"يبدو التحميل والخطأ متماثلين، فلا تستطيع الشاشة عرض الرسالة الصحيحة."},{"text":"مصفوفة <code dir=\"ltr\">trails</code> واحدة تبدأ فارغة.","why":"المصفوفة الفارغة لا تميّز بين «ما زال يُحمّل» و«لا دروب قريبة»، وهما شاشتان مختلفتان جدًا."}],"answer":0}],"exercise":{"kind":"web","mode":"js","starter":{"js":"const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\n\n// Run task(attempt). Retry failures with a doubling delay; give up fast on errors\n// that can't succeed later. `sleep` is injectable so tests don't really wait.\nasync function withRetry(task, { retries = 2, baseDelayMs = 1000, sleep = wait } = {}) {\n  return task(1);\n}\n\n// Demo: fails twice with a 503, then succeeds.\nlet calls = 0;\nconst flaky = async () => {\n  calls += 1;\n  if (calls < 3) throw Object.assign(new Error('Service unavailable'), { status: 503 });\n  return ['Ridge Loop', 'Lake Trail'];\n};\nwithRetry(flaky, { baseDelayMs: 10 }).then((v) => console.log('got', v, 'after', calls, 'calls'), (e) => console.log('failed:', e.message));\n"},"solution":{"js":"const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\n\nfunction isRetryable(error) {\n  const status = error && error.status;\n  if (typeof status !== 'number') return true; // network failure, timeout\n  if (status === 408 || status === 429) return true;\n  return status < 400 || status >= 500;\n}\n\nasync function withRetry(task, { retries = 2, baseDelayMs = 1000, sleep = wait } = {}) {\n  for (let attempt = 1; ; attempt++) {\n    try {\n      return await task(attempt);\n    } catch (error) {\n      if (attempt > retries || !isRetryable(error)) throw error;\n      await sleep(baseDelayMs * 2 ** (attempt - 1));\n    }\n  }\n}\n\nlet calls = 0;\nconst flaky = async () => {\n  calls += 1;\n  if (calls < 3) throw Object.assign(new Error('Service unavailable'), { status: 503 });\n  return ['Ridge Loop', 'Lake Trail'];\n};\nwithRetry(flaky, { baseDelayMs: 10 }).then((v) => console.log('got', v, 'after', calls, 'calls'), (e) => console.log('failed:', e.message));\n"},"checks":[{"id":"returns-value","test":"const delays = []; const sleep = async (ms) => { delays.push(ms); }; const fail = (status) => Object.assign(new Error('x' + status), { status }); const v = await withRetry(async () => 'ok', { sleep }); return v === 'ok' && delays.length === 0;"},{"id":"retries-then-succeeds","test":"const delays = []; const sleep = async (ms) => { delays.push(ms); }; const fail = (status) => Object.assign(new Error('x' + status), { status }); let n = 0; const v = await withRetry(async (attempt) => { n++; if (attempt < 3) throw fail(503); return 'done'; }, { retries: 2, baseDelayMs: 100, sleep }); return v === 'done' && n === 3;"},{"id":"backoff","test":"const delays = []; const sleep = async (ms) => { delays.push(ms); }; const fail = (status) => Object.assign(new Error('x' + status), { status }); let n = 0; try { await withRetry(async () => { n++; throw new Error('offline'); }, { retries: 3, baseDelayMs: 100, sleep }); } catch {} return n === 4 && delays.join() === '100,200,400';"},{"id":"gives-up","test":"const delays = []; const sleep = async (ms) => { delays.push(ms); }; const fail = (status) => Object.assign(new Error('x' + status), { status }); let caught; try { await withRetry(async () => { throw fail(500); }, { retries: 1, baseDelayMs: 1, sleep }); } catch (e) { caught = e; } return caught instanceof Error && caught.status === 500;"},{"id":"no-retry-4xx","test":"const delays = []; const sleep = async (ms) => { delays.push(ms); }; const fail = (status) => Object.assign(new Error('x' + status), { status }); let n = 0; let caught; try { await withRetry(async () => { n++; throw fail(404); }, { retries: 3, sleep }); } catch (e) { caught = e; } return n === 1 && caught?.status === 404 && delays.length === 0;"},{"id":"retry-429","test":"const delays = []; const sleep = async (ms) => { delays.push(ms); }; const fail = (status) => Object.assign(new Error('x' + status), { status }); let n = 0; const v = await withRetry(async () => { n++; if (n === 1) throw fail(429); if (n === 2) throw fail(408); return 'ok'; }, { retries: 2, baseDelayMs: 50, sleep }); return v === 'ok' && n === 3 && delays.join() === '50,100';"}],"title":"أعِد محاولة الطلبات المتعثّرة مع تأخير متزايد","prompt":"<p>اكتب <code dir=\"ltr\">withRetry(task, { retries, baseDelayMs, sleep })</code> لاستدعاءات الـ API في Trailhead على إشارة الجبال الضعيفة.</p>\n<ul>\n<li>استدعِ <code dir=\"ltr\">task(attempt)</code>، مع <code dir=\"ltr\">attempt</code> تبدأ من 1، وأرجع نتيجتها.</li>\n<li>إن رفضت، فأعِد المحاولة حتى <code dir=\"ltr\">retries</code> مرة إضافية. قبل كل إعادة، نفّذ <code dir=\"ltr\">await sleep(ms)</code> حيث يتضاعف التأخير: <code dir=\"ltr\">baseDelayMs</code>، ثم <code dir=\"ltr\">2 × baseDelayMs</code>، ثم <code dir=\"ltr\">4 ×</code>… (للقيمة 100: 100، 200، 400).</li>\n<li>الأخطاء التي فيها <code dir=\"ltr\">status</code> رقمية من 400 إلى 499 لا يمكن أن تنجح بإعادة المحاولة: أعد رميها <strong>فورًا</strong>. والاستثناءان هما <code dir=\"ltr\">408</code> (انتهاء المهلة) و<code dir=\"ltr\">429</code> (طلبات كثيرة جدًا)، فتُعاد محاولتهما.</li>\n<li>الأخطاء التي ليس فيها <code dir=\"ltr\">status</code> (أعطال الشبكة) وأخطاء 5xx تُعاد محاولتها.</li>\n<li>حين تفشل كل المحاولات، ارفض بـ <strong>آخر</strong> خطأ.</li>\n</ul>\n<p>انتظر دائمًا باستخدام الـ <code dir=\"ltr\">sleep</code> المعطاة لك، لكي تستطيع الفحوص تسجيل التأخيرات.</p>\n","hints":["حلقة <code dir=\"ltr\">for (let attempt = 1; ; attempt++)</code> وبداخلها <code dir=\"ltr\">try { return await task(attempt); } catch (error) { … }</code> أبسط قراءةً من التعاود.","في الـ <code dir=\"ltr\">catch</code>، استسلم (<code dir=\"ltr\">throw error</code>) حين تكون <code dir=\"ltr\">attempt &gt; retries</code> أو حين لا يكون الخطأ قابلًا لإعادة المحاولة؛ وإلا <code dir=\"ltr\">await sleep(baseDelayMs * 2 ** (attempt - 1))</code>.","اكتب <code dir=\"ltr\">isRetryable(error)</code> منفصلة: لا status رقمية → true؛ 408 أو 429 → true؛ وإلا فأعِد المحاولة فقط حين تكون <code dir=\"ltr\">status &gt;= 500</code>."],"explanation":"<p>يمنح التأخير المتزايد أُسّيًا (exponential backoff) انقطاعَ الإشارة القصير وقتًا للتعافي دون إغراق خادم يعاني، وتصنيف الأخطاء يمنع التطبيق من إعادة محاولة طلبات لن تنجح أبدًا: فالـ 404 أو 401 يفشل بالطريقة نفسها ثلاث مرات، لكن أبطأ. وحقن <code dir=\"ltr\">sleep</code> قرار تصميمي صغير بعائد كبير: بيئة الإنتاج تستخدم مؤقّتًا حقيقيًا، والاختبارات تعمل فورًا مع التحقّق من التأخيرات بدقّة. ومكتبات مثل TanStack Query تطبّق السياسة نفسها نيابةً عنك.</p>\n","checkLabels":{"returns-value":"المهمّة الناجحة تُرجع قيمتها دون انتظار","retries-then-succeeds":"المهمّة التي تفشل مرتين ثم تنجح تُرجع قيمتها","backoff":"تتضاعف التأخيرات بين المحاولات (100، 200، 400)","gives-up":"بعد المحاولة الأخيرة، يُرمى آخر خطأ","no-retry-4xx":"يُرمى الـ 404 فورًا دون إعادة محاولة","retry-429":"تُعاد محاولة استجابات 429 و408"}},"words":578,"readMinutes":3}