الوحدة 3 · التنقّل والـ State
الـ tabs والمسارات الديناميكية والـ params
19 دقراءة 3 دبيئة تجريب الكود
الدرس في لمحة
أضف إلى Trailhead أشرطة tabs سفلية باستخدام مجموعة مسارات، وشاشة لتفاصيل الرحلة عبر مسار ديناميكي [id]، واقرأ الـ params الخاصة بها بأمان، متذكّرًا أن الـ params تصل دائمًا نصوصًا.
- 1مجموعة مسارات للـ tabs
- 2المسارات الديناميكية والـ params
- 3المعرّفات في الـ params، والبيانات من الـ state
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
صار في Trailhead الآن أكثر من مكان يمكن أن تكون فيه: سجلّ الرحلات، وصفحة الإحصاءات، والإعدادات، إضافةً إلى شاشة تفاصيل لكل رحلة. الثلاثة الأولى نظائر يتنقّل المستخدمون بينها طوال الوقت؛ أما شاشة التفاصيل فشيء تتعمّق فيه ثم تعود منه. هذا هو الشكل الكلاسيكي لتطبيقات الجوّال: tabs للنظائر، وstack للتعمّق.
مجموعة مسارات للـ tabs
هذه بنية الملفات التي يستخدمها Trailhead:
src/app/
_layout.tsx root Stack
(tabs)/
_layout.tsx Tabs navigator
index.tsx / (Log)
stats.tsx /stats
settings.tsx /settings
hikes/
[id].tsx /hikes/42 (pushed over the tabs)المجلّد بين قوسين مجموعة مسارات (route group). تتيح لشاشات الـ tabs الثلاث أن تتشارك layout دون أن تضيف شيئًا إلى عناوينها: (tabs)/stats.tsx هو /stats، لا /tabs/stats.
يُرجع layout الـ tabs أداة التنقّل Tabs، مع Tabs.Screen واحد لكل tab، بالترتيب الذي يجب أن تظهر به:
import Ionicons from '@expo/vector-icons/Ionicons';
import { Tabs } from 'expo-router';
export default function TabLayout() {
return (
<Tabs screenOptions={{ tabBarActiveTintColor: '#2f6f4e' }}>
<Tabs.Screen
name="index"
options={{
title: 'Log',
tabBarIcon: ({ color, size }) => <Ionicons name="list" color={color} size={size} />,
}}
/>
<Tabs.Screen
name="stats"
options={{
title: 'Stats',
tabBarIcon: ({ color, size }) => <Ionicons name="stats-chart" color={color} size={size} />,
}}
/>
<Tabs.Screen
name="settings"
options={{
title: 'Settings',
tabBarIcon: ({ color, size }) => <Ionicons name="settings-outline" color={color} size={size} />,
}}
/>
</Tabs>
);
}ثم يضع الـ root layout مجموعة الـ tabs كلها داخل الـ Stack، مع إخفاء ترويسة المجموعة نفسها (فلكل tab ترويسته)، ويليها مسار التفاصيل:
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="hikes/[id]" options={{ title: 'Hike' }} />
</Stack>
);
}ولأن hikes/[id] ينتمي إلى الـ Stack الجذري لا إلى الـ tabs، فإن فتح رحلة يُدخل شاشة كاملة منزلقة فوق شريط الـ tabs، وتعيدك إيماءة الرجوع إلى الـ tab الذي جئت منه. هذا ما تفعله معظم تطبيقات iOS وAndroid، ويعطي شاشة التفاصيل ارتفاع الهاتف كله.
عادتان تُبقيان الـ tabs مريحة. اجعلها بين ثلاث وخمس وجهات رئيسية يزورها المستخدمون كثيرًا؛ فالـ tab السادس يعني عادةً أن بعضها مكانه داخل الإعدادات. وتذكّر أن الـ tabs تبقى مركّبة بعد زيارتها: انتقل من Log إلى Stats ثم عُد، وسيحتفظ السجلّ بموضع تمريره. هذا هو السلوك الذي يتوقّعه المستخدمون، فلا تُعِد ضبطه. وإن احتاج tab واحد إلى شاشات تعمّق خاصة به تُبقي شريط الـ tabs ظاهرًا، فأعطِ ذلك الـ tab مجلّدًا بملف _layout.tsx خاص به يُرجع Stack؛ فتداخل أدوات التنقّل ليس إلا تداخلًا للمجلّدات.
المسارات الديناميكية والـ params
الملف المسمّى بأقواس مربّعة، [id].tsx، يطابق أي جزء مفرد من الـ URL ويكشفه بوصفه param اسمه id. اقرأه باستخدام useLocalSearchParams:
import { Stack, useLocalSearchParams } from 'expo-router';
import { Text, View } from 'react-native';
import { useHike } from '../../state/hikes';
export default function HikeDetail() {
const { id } = useLocalSearchParams<{ id: string }>();
const hike = useHike(id);
if (!hike) {
return (
<View style={{ flex: 1, padding: 24 }}>
<Text>That hike doesn't exist anymore.</Text>
</View>
);
}
return (
<View style={{ flex: 1, padding: 16 }}>
<Stack.Screen options={{ title: hike.name }} />
<Text style={{ fontSize: 16 }}>{hike.distanceKm} km</Text>
</View>
);
}وللوصول إليها، اربط إلى الـ URL. إما أن تبني النص بنفسك، أو تدع Expo Router يملأ النمط:
<Link href={`/hikes/${hike.id}`}>{hike.name}</Link>
<Link href={{ pathname: '/hikes/[id]', params: { id: hike.id } }}>{hike.name}</Link>أي شيء في params ليس جزءًا من المسار يصبح query string: فـ params: { id: hike.id, units: 'mi' } ينتج /hikes/42?units=mi، وتقرأ شاشة التفاصيل units بالـ hook نفسه.
المعرّفات في الـ params، والبيانات من الـ state
مرّر أصغر شيء يعرّف البيانات، وهو معرّف في العادة، وابحث عن البيانات في الشاشة الوجهة. يبدو تمرير كائن رحلة كامل مريحًا، لكن الـ params تُحوَّل إلى نصّ داخل الـ URL: يصبح الكائن نصًّا، ولا يستطيع deep link إعادة إنشائه أبدًا، وإن عدّل المستخدم الرحلة، تعرض شاشة التفاصيل النسخة القديمة التي سُلّمت لها. الدرس التالي يبني الـ state المشتركة التي يقرأ منها useHike(id).
يُرجع useLocalSearchParams الـ params الخاصة بالشاشة التي يُستدعى فيها، وهذا ما تريده في الغالبية العظمى من الحالات. أما شقيقه useGlobalSearchParams فيتحدّث كلما تغيّر أي مسار، حتى المسارات في الخلفية، وهذا يسبّب عمليات render إضافية؛ احتفظ به لأشياء مثل التحليلات (analytics) التي تهتم فعلًا بكل عملية تنقّل.
التالي: الـ state التي تقف وراء عمليات البحث هذه، أي مصدر واحد للحقيقة عن الرحلات، تتشاركه كل الـ tabs والشاشات.
أهم النقاط
- المجلّد بين قوسين، مثل
(tabs)، يجمّع المسارات تحت layout دون أن يضيف جزءًا إلى الـ URL. - ضع أداة تنقّل الـ tabs في
(tabs)/_layout.tsx، وأضف شاشات التفاصيل من الـ Stack الجذري لتغطّي شريط الـ tabs. - الملف المسمّى
[id].tsxيطابق أي جزء مفرد من الـ URL؛ اقرأه باستخدامuseLocalSearchParams(). - الـ params في المسارات نصوص (أو مصفوفات من النصوص)، فحوّلها وتحقّق منها قبل الاستخدام.
- مرّر المعرّفات في الـ params، لا الكائنات كاملة أبدًا، وابحث عن البيانات في الشاشة الوجهة.