الوحدة 3 · التنقّل والـ State
التوجيه المبني على الملفات في Expo Router
18 دقراءة 4 داملأ الفراغات
الدرس في لمحة
حوّل الملفات داخل src/app إلى شاشات Trailhead باستخدام Expo Router، وغلّفها بـ stack أصلي عبر ملف layout، وتنقّل بينها باستخدام Link وكائن router.
- 1الملفات هي المسارات
- 2الـ layouts تغلّف ما يجاورها
- 3التنقّل بين الشاشات
- 4الـ stack ليس سجلّ المتصفّح
- 5شاشة «غير موجود» ولماذا ما زالت الـ URLs مهمّة
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
على الويب، يُظهر شريط العنوان مكانك، ويمشي زرّ الرجوع عبر السجلّ. أما على الهاتف فلا يرى المستخدمون أيًّا منهما: يرون شاشات تنزلق من اليمين، وسهم رجوع أو إيماءة سحب، وtabs في الأسفل. وتحت السطح، ما زال Expo Router يعطي كل شاشة URL، وهذا ما يجعل الـ deep links والتنقّل المضبوط بالأنواع ونسخة الويب ممكنة. تحصل على النموذجين معًا.
الملفات هي المسارات
يبني Expo Router نظام التنقّل من الملفات الموجودة في src/app. مسار الملف هو المسار (route):
src/app/
_layout.tsx wraps everything below in a navigator
index.tsx /
about.tsx /about
settings/
_layout.tsx wraps the settings screens
index.tsx /settings
units.tsx /settings/units
+not-found.tsx any URL that doesn't matchكل ملف مسار يصدّر component بشكل افتراضي (default export). وهذا كل ما تعنيه الشاشة:
import { Text, View } from 'react-native';
export default function About() {
return (
<View style={{ flex: 1, padding: 16 }}>
<Text>Trailhead keeps a private log of your hikes.</Text>
</View>
);
}ولأن كل ملف في المجلّد يصبح مسارًا، أبقِ كل ما عداه في مكان آخر: src/components، وsrc/lib، وsrc/state. component صفّ حُفظ خطأً في src/app يصبح شاشة يستطيع أي أحد الوصول إليها عبر deep link.
الـ layouts تغلّف ما يجاورها
يرسم ملف _layout.tsx أداة تنقّل حول المسارات الموجودة في مجلّده. يستخدم الـ root layout في Trailhead Stack، وهو أداة التنقّل الأصلية التي تضيف الشاشات وتُزيلها بحركات المنصّة، مع السحب للرجوع على iOS وإيماءة الرجوع على مستوى النظام في Android:
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<Stack screenOptions={{ headerTintColor: '#2f6f4e' }}>
<Stack.Screen name="index" options={{ title: 'Trailhead' }} />
<Stack.Screen name="about" options={{ title: 'About' }} />
</Stack>
);
}تنطبق screenOptions على كل شاشة في الـ stack؛ وكل Stack.Screen يخصّص شاشة واحدة، تُطابَق باسم مسارها name. لست مضطرًا إلى إدراج كل مسار: المسارات غير المدرجة تعمل مع ذلك بالخيارات الافتراضية.
بضعة خيارات للترويسة تغطّي معظم الاحتياجات: title، وheaderShown: false للشاشات التي ترسم شريطها العلوي بنفسها (مثل الخريطة بملء الشاشة من الدرس السابق)، وheaderBackTitle للتسمية بجوار سهم الرجوع في iOS، وheaderRight لزرّ مثل “Edit”. وعلى iOS يعطيك headerLargeTitle: true العنوان الكبير القابل للطي الذي تراه في تطبيقات Apple نفسها، وهو يناسب قائمة رئيسية مثل سجلّ الرحلات.
وحين يعتمد العنوان على البيانات (اسم الرحلة مثلًا)، ارسم <Stack.Screen options={{ title: hike.name }} /> داخل الشاشة نفسها. لا يرسم شيئًا مرئيًا؛ بل يضبط ترويسة الشاشة التي هو فيها.
التنقّل بين الشاشات
لكل ما ينقر عليه المستخدم، استخدم Link. يرسم نصًّا افتراضيًا؛ ومع asChild يسلّم التنقّل إلى الـ component القابل للضغط الخاص بك:
import { Link } from 'expo-router';
import { Pressable, Text, View } from 'react-native';
export default function HikeLog() {
return (
<View style={{ flex: 1, padding: 16, gap: 12 }}>
<Link href="/about">About Trailhead</Link>
<Link href="/settings" asChild>
<Pressable accessibilityRole="button">
<Text>Settings</Text>
</Pressable>
</Link>
</View>
);
}ومن الكود، بعد حفظ أو تسجيل دخول، استخدم الكائن router:
import { router } from 'expo-router';
router.push('/settings/units'); // add a screen on top
router.back(); // pop the top screen
router.replace('/'); // swap the current screen; no way back to itpush هو الانتقال الأمامي المعتاد. وreplace للمسارات التي لا معنى للعودة فيها: بعد الشاشات التعريفية، وبعد تسجيل الدخول، وبعد تأكيد «تم الحفظ!». أما back فهو ما يفعله سهم الرجوع في الترويسة وإيماءة السحب أصلًا؛ استدعه بنفسك بعد حفظ نموذج ليعود المستخدم إلى حيث جاء. وهناك أيضًا router.navigate، الذي يذهب إلى مسار، وإن كان ذلك المسار موجودًا في الـ stack أصلًا، يعود إليه بدلًا من إضافة نسخة مكرّرة.
الـ stack ليس سجلّ المتصفّح
من المغري أن تعامل الـ stack كسجلّ المتصفّح، لكن هناك فرقًا واحدًا مهمًّا: كل شاشة في الـ stack تبقى مركّبة. حين تضيف شاشة تفاصيل الرحلة، يحتفظ السجلّ تحتها بالـ state وموضع التمرير، وتستمر الـ effects الخاصة به في العمل. ولهذا يكون الرجوع فوريًا، ولهذا أيضًا لا يُعاد تشغيل useEffect في الشاشة حين تعود إليها. وحين تحتاج إلى تحديث البيانات في كل مرة تعود فيها الشاشة إلى الواجهة، يوفّر Expo Router الـ hook useFocusEffect، الذي يعمل حين تحصل الشاشة على التركيز (focus) وينظّف حين تفقده.
شاشة «غير موجود» ولماذا ما زالت الـ URLs مهمّة
يُرسم +not-found.tsx لأي URL لا يطابق مسارًا، وهذا يحدث على الجوّال غالبًا عبر deep links من رسائل بريد قديمة أو من شاشة غُيّر اسمها. أعطه رسالة ودودة وLink يعود إلى الرئيسية.
والتفكير بالـ URLs يؤتي ثماره لاحقًا في هذا القسم: يستطيع إشعار أن يفتح /hikes/42 مباشرة، ويستطيع رابط في رسالة أن يفتح التطبيق على الشاشة الصحيحة، وتعمل المسارات نفسها على الويب إن بنيت Trailhead له يومًا.
في الدرس التالي ستضيف tabs في أسفل التطبيق، وشاشة تفاصيل لكل رحلة، وتمرّر معرّف الرحلة عبر الـ URL.
أهم النقاط
- كل ملف داخل
src/appهو مسار (route)، ومساره هو الـ URL؛ أبقِ الـ components والـ hooks والأدوات المساعدة خارج ذلك المجلّد. - يغلّف ملف
_layout.tsxالمسارات المجاورة له بأداة تنقّل (navigator)، مثلStackأصلي. - استخدم
<Link href>للتنقّل الذي ينقر عليه المستخدم، واستخدمrouter.pushوrouter.replaceوrouter.backمن الكود. pushيضيف شاشة إلى الـ stack، وbackيُزيلها، وreplaceيستبدل الشاشة الحالية فلا يستطيع المستخدم العودة إليها.