أساسيات React Native
الآن بعد أن أعددنا بيئة التطوير لدينا، يمكننا الخوض في أساسيات React Native والبدء بتطوير تطبيقنا. في هذا القسم، سنتعلم كيفية بناء واجهات المستخدم باستخدام المكوّنات الأساسية في React Native، وكيفية إضافة خصائص الأنماط إلى هذه المكوّنات الأساسية، وكيفية الانتقال بين العروض، وكيفية إدارة حالة النموذج بكفاءة.
المكوّنات الأساسية
تعلمنا في الأجزاء السابقة أنه يمكننا استخدام React لتعريف المكوّنات كدوال، تستقبل props كوسيط وتُعيد شجرة من عناصر React. وتُمثَّل هذه الشجرة عادةً بصيغة JSX. وفي بيئة المتصفح، استخدمنا مكتبة ReactDOM لتحويل هذه المكوّنات إلى شجرة DOM يمكن للمتصفح عرضها. وإليك مثالاً ملموساً على مكوّن بسيط جداً:
const HelloWorld = props => {
return <div>Hello world!</div>;
};
يعيد مكوّن HelloWorld عنصر div وحيداً أُنشئ باستخدام صيغة JSX. وقد نتذكر أن صيغة JSX هذه تُصرَّف إلى استدعاءات للدالة React.createElement، مثل هذا:
React.createElement('div', null, 'Hello world!');
ينشئ هذا السطر من الشيفرة عنصر div دون أي props ومع عنصر ابن وحيد هو نص "Hello world". وعندما نعرض هذا المكوّن في عنصر DOM جذري باستخدام الدالة render، سيُعرض عنصر div كعنصر DOM المقابل له.
وكما نرى، فإن React غير مرتبطة ببيئة معينة مثل بيئة المتصفح. وبدلاً من ذلك، هناك مكتبات مثل ReactDOM يمكنها عرض مجموعة من المكوّنات المعرّفة مسبقاً، مثل عناصر DOM، في بيئة محددة. وفي React Native تُسمّى هذه المكوّنات المعرّفة مسبقاً المكوّنات الأساسية.
المكوّنات الأساسية هي مجموعة من المكوّنات التي يوفّرها React Native، والتي تستخدم خلف الكواليس المكوّنات الأصلية للمنصة. لنُنفّذ المثال السابق باستخدام React Native:
import { Text } from 'react-native';
const HelloWorld = props => {
return <Text>Hello world!</Text>;
};
إذن نستورد مكوّن Text من React Native ونستبدل عنصر div بعنصر Text. ولكثير من عناصر DOM المألوفة نظيراتها في React Native. وإليك بعض الأمثلة المختارة من وثائق المكوّنات الأساسية في React Native:
- مكوّن Text هو المكوّن الوحيد في React Native الذي يمكن أن يحتوي على أبناء نصية. وهو مشابه مثلاً لعنصري
<strong>و<h1>. - مكوّن View هو لبنة بناء واجهة المستخدم الأساسية المشابهة لعنصر
<div>. - مكوّن TextInput هو مكوّن حقل نصي مشابه لعنصر
<input>. - مكوّن Pressable مخصّص لالتقاط أحداث الضغط المختلفة. وهو مشابه مثلاً لعنصر
<button>.
هناك بعض الفروق الجديرة بالملاحظة بين المكوّنات الأساسية وعناصر DOM. الفرق الأول أن مكوّن Text هو المكوّن الوحيد في React Native الذي يمكن أن يحتوي على أبناء نصية. وهذا يعني أنه لا يمكنك، مثلاً، استبدال مكوّن Text بمكوّن View في المثال السابق.
الفرق الثاني الجدير بالملاحظة يتعلق بمعالجات الأحداث. فعند العمل مع عناصر DOM اعتدنا إضافة معالجات أحداث مثل onClick إلى أي عنصر تقريباً مثل <div> و<button>. أما في React Native فعلينا قراءة وثائق API بعناية لمعرفة معالجات الأحداث (وكذلك props الأخرى) التي يقبلها المكوّن. فعلى سبيل المثال، يوفّر مكوّن Pressable خصائص للاستماع إلى أنواع مختلفة من أحداث الضغط. ويمكننا مثلاً استخدام خاصية onPress في المكوّن للاستماع إلى أحداث الضغط:
import { Text, Pressable, Alert } from 'react-native';
const PressableText = props => {
return (
<Pressable
onPress={() => Alert.alert('You pressed the text!')}
>
<Text>You can press me</Text>
</Pressable>
);
};
تثبيت الاعتماديات في مشروع Expo
في الأجزاء السابقة من المقرر، ثبّتنا المكتبات أساساً كاعتماديات للمشروع باستخدام الأمر npm install. غير أنه عند تثبيت مكتبات Expo وReact Native، يُوصى باستخدام الأمر npx expo install بدلاً من ذلك. فهذا يتيح لواجهة سطر أوامر Expo اختيار إصدار من المكتبة يتوافق مع المشروع وإصدار SDK الخاص به.
سنحتاج قريباً إلى مكتبة expo-constants التي تزوّد التطبيق بمعلومات البيئة مثل ارتفاع شريط الحالة الصحيح. ثبّت المكتبة بالأمر:
npx expo install expo-constants
وإذا لم تكن متأكداً مما إذا كانت المكتبة تحتوي على شيفرة أصلية خاصة بـ Expo أو React Native، فيمكنك دائماً تثبيتها باستخدام الأمر npx expo install. وإذا لم يتعرّف Expo على الحزمة، فسيعود إلى تثبيتها باستخدام الأمر العادي npm install.
هيكلة مشروعنا
الآن بعد أن اكتسبنا فهماً أساسياً للمكوّنات الأساسية، لنبدأ بإعطاء مشروعنا بعض الهيكلة. أنشئ مجلد src في المجلد الجذري لمشروعك، وأنشئ داخل مجلد src مجلد components.
وفي مجلد components أنشئ ملف Main.jsx بالمحتوى التالي:
import Constants from 'expo-constants';
import { Text, StyleSheet, View } from 'react-native';
const styles = StyleSheet.create({
container: {
marginTop: Constants.statusBarHeight,
flex: 1,
},
});
const Main = () => {
return (
<View style={styles.container}>
<Text>Rate Repository Application</Text>
</View>
);
};
export default Main;
بعد ذلك، لنستخدم مكوّن Main في مكوّن App داخل ملف App.js الموجود في المجلد الجذري لمشروعنا. استبدل المحتوى الحالي للملف بهذا:
import Main from './src/components/Main';
const App = () => {
return <Main />;
};
export default App;
إعادة تحميل التطبيق يدوياً
كما رأينا، سيعيد Expo تحميل التطبيق تلقائياً عند إجراء تغييرات على الشيفرة. غير أنه قد تكون هناك أوقات لا تعمل فيها إعادة التحميل التلقائي، فيلزم إعادة تحميل التطبيق يدوياً. وفي واجهة سطر أوامر Expo، يمكنك الضغط على r لإعادة التحميل؛ وهذا يفعّل إعادة التحميل عادةً.
ويمكن تحقيق ذلك أيضاً عبر قائمة المطوّر داخل التطبيق. ويمكنك الوصول إلى قائمة المطوّر بهزّ جهازك أو باختيار "Shake Gesture" داخل قائمة Hardware في محاكي iOS. ويمكنك أيضاً استخدام اختصار لوحة المفاتيح ⌘D عندما يعمل تطبيقك في محاكي iOS، أو ⌘M عند التشغيل في محاكي Android على نظام Mac OS، وCtrl+M على Windows وLinux.
وبمجرد فتح قائمة المطوّر، اضغط ببساطة "Reload" لإعادة تحميل التطبيق. وبعد إعادة تحميل التطبيق، ينبغي أن تعمل عمليات إعادة التحميل التلقائية دون الحاجة إلى إعادة تحميل يدوية.
3. قائمة المستودعات المُقيَّمة
الأنماط
الآن بعد أن أصبح لدينا فهم أساسي لكيفية عمل المكوّنات الأساسية ويمكننا استخدامها لبناء واجهة مستخدم بسيطة، حان وقت إضافة بعض الأنماط. في الجزء 2 تعلمنا أنه في بيئة المتصفح يمكننا تعريف خصائص أنماط مكوّنات React باستخدام CSS. وكان لدينا خيار تعريف هذه الأنماط مضمّنة باستخدام خاصية style أو في ملف CSS بمحدّد مناسب.
هناك أوجه تشابه كثيرة في طريقة إرفاق خصائص الأنماط بالمكوّنات الأساسية في React Native وطريقة إرفاقها بعناصر DOM. ففي React Native تقبل معظم المكوّنات الأساسية خاصية تُسمّى style. وتقبل خاصية style كائناً يحوي خصائص الأنماط وقيمها. وهذه الخصائص هي في معظم الحالات نفسها كما في CSS، غير أن أسماء الخصائص تُكتب بصيغة camelCase. وهذا يعني أن خصائص CSS مثل padding-top وfont-size تُكتب paddingTop وfontSize. وإليك مثالاً بسيطاً على كيفية استخدام خاصية style:
import { Text, View } from 'react-native';
const BigBlueText = () => {
return (
<View style={{ padding: 20 }}>
<Text style={{ color: 'blue', fontSize: 24, fontWeight: '700' }}>
Big blue text
</Text>
</View>
);
};
إضافةً إلى أسماء الخصائص، ربما لاحظت فرقاً آخر في المثال. ففي CSS تكون القيم الرقمية للخصائص عادةً مصحوبة بوحدة مثل px أو % أو em أو rem. أما في React Native فجميع قيم الخصائص المتعلقة بالأبعاد مثل width وheight وpadding وmargin وكذلك أحجام الخطوط تكون دون وحدة. وتمثّل هذه القيم الرقمية بلا وحدة بكسلات مستقلة عن الكثافة. وإذا كنت تتساءل عن خصائص الأنماط المتاحة لمكوّنات أساسية معينة، فراجع ورقة أنماط React Native المرجعية.
بشكل عام، لا يُعدّ تعريف الأنماط مباشرة في خاصية style فكرة جيدة، لأنه يجعل المكوّنات متضخمة وغير واضحة. وبدلاً من ذلك، ينبغي تعريف الأنماط خارج دالة عرض المكوّن باستخدام الدالة StyleSheet.create. وتقبل الدالة StyleSheet.create وسيطاً وحيداً هو كائن يتألف من كائنات أنماط مُسمّاة، وهي تُنشئ مرجع نمط StyleSheet من الكائن المعطى. وإليك مثالاً على كيفية إعادة هيكلة المثال السابق باستخدام الدالة StyleSheet.create:
import { Text, View, StyleSheet } from 'react-native';
const styles = StyleSheet.create({
container: {
padding: 20,
},
text: {
color: 'blue',
fontSize: 24,
fontWeight: '700',
},
});
const BigBlueText = () => {
return (
<View style={styles.container}>
<Text style={styles.text}>
Big blue text
</Text>
</View>
);
};
أنشأنا كائني نمط مُسمّيين، styles.container وstyles.text. ويمكننا داخل المكوّن الوصول إلى كائنات أنماط محددة بالطريقة نفسها التي نصل بها إلى أي مفتاح في كائن عادي.
إضافةً إلى كائن، تقبل خاصية style أيضاً مصفوفة من الكائنات. وفي حالة المصفوفة، تُدمج الكائنات من اليسار إلى اليمين بحيث تكون أولوية خصائص الأنماط اللاحقة أعلى. ويعمل هذا بشكل تعاودي، فيمكن أن تكون لدينا مثلاً مصفوفة تحتوي على مصفوفة من الأنماط وهكذا. وإذا احتوت المصفوفة على قيم تُقيَّم إلى false، مثل null أو undefined، فتُتجاهل هذه القيم. وهذا يسهّل تعريف أنماط شرطية مثلاً بناءً على قيمة prop. وإليك مثالاً على الأنماط الشرطية:
import { Text, StyleSheet } from 'react-native';
const styles = StyleSheet.create({
text: {
color: 'grey',
fontSize: 14,
},
blueText: {
color: 'blue',
},
bigText: {
fontSize: 24,
fontWeight: '700',
},
});
const FancyText = ({ isBlue, isBig, children }) => {
const textStyles = [
styles.text,
isBlue && styles.blueText,
isBig && styles.bigText,
];
return <Text style={textStyles}>{children}</Text>;
};
const Main = () => {
return (
<>
<FancyText>Simple text</FancyText>
<FancyText isBlue>Blue text</FancyText>
<FancyText isBig>Big text</FancyText>
<FancyText isBig isBlue>
Big blue text
</FancyText>
</>
);
};
تُعرَّف props الآن دون قيمة صريحة:
<FancyText isBlue>Blue text</FancyText>
في JSX، تزويد prop بدون قيمة هو صيغة خاصة تعني نفس معنى ={true}. ولذلك فإن السطرين التاليين متكافئان:
<FancyText isBlue>Blue text</FancyText>
<FancyText isBlue={true}>Blue text</FancyText>
في المثال، نستخدم المعامل && مع التعبير condition && exprIfTrue:
const textStyles = [
styles.text,
isBlue && styles.blueText,
isBig && styles.bigText,
];
فمثلاً، في السطر المميّز، يعطي التعبير styles.blueText إذا قُيّمت الشرط isBlue إلى true، وإلا فإنه يعطي condition، وهي في تلك الحالة قيمة تُقيَّم إلى false. وهذا اختصار عملي وواسع الانتشار للغاية.
وخيار آخر هو استخدام المعامل الشرطي هكذا:
condition ? exprIfTrue : exprIfFalse
واجهة مستخدم متسقة باستخدام الثيم
لنبقَ مع مفهوم الأنماط لكن من منظور أوسع قليلاً. معظمنا استخدم عدداً كبيراً من التطبيقات المختلفة وقد نتفق على أن إحدى السمات التي تصنع واجهة مستخدم جيدة هي الاتساق. وهذا يعني أن مظهر مكوّنات واجهة المستخدم مثل حجم الخط وعائلة الخط واللون يتبع نمطاً متسقاً. ولتحقيق ذلك علينا بطريقة ما تحديد قيم خصائص الأنماط المختلفة كمعاملات. وتُعرف هذه الطريقة عادةً بـ الثيم (theming).
قد يكون مستخدمو مكتبات واجهات المستخدم الشائعة مثل Bootstrap وMaterial UI على دراية كبيرة بالثيم أصلاً. ومع أن تطبيقات الثيم تختلف، فإن الفكرة الرئيسية هي دائماً استخدام متغيرات مثل colors.primary بدلاً من "الأرقام السحرية" مثل #0366d6 عند تعريف الأنماط. وهذا يؤدي إلى زيادة الاتساق والمرونة.
لنرَ كيف يمكن أن يعمل الثيم عملياً في تطبيقنا. سنستخدم الكثير من النصوص بتنويعات مختلفة، مثل أحجام وألوان خطوط مختلفة. ولأن React Native لا يدعم الأنماط العامة، ينبغي أن ننشئ مكوّن Text خاصاً بنا للحفاظ على اتساق المحتوى النصي. لنبدأ بإضافة كائن إعداد الثيم التالي في ملف theme.js في مجلد src:
const theme = {
colors: {
textPrimary: '#24292e',
textSecondary: '#586069',
primary: '#0366d6',
},
fontSizes: {
body: 14,
subheading: 16,
},
fonts: {
main: 'System',
},
fontWeights: {
normal: '400',
bold: '700',
},
};
export default theme;
بعد ذلك، ينبغي إنشاء مكوّن Text الفعلي الذي يستخدم إعداد الثيم هذا. أنشئ ملف Text.jsx في مجلد components حيث لدينا بالفعل بقية مكوّناتنا. وأضف المحتوى التالي إلى ملف Text.jsx:
import { Text as NativeText, StyleSheet } from 'react-native';
import theme from '../theme';
const styles = StyleSheet.create({
text: {
color: theme.colors.textPrimary,
fontSize: theme.fontSizes.body,
fontFamily: theme.fonts.main,
fontWeight: theme.fontWeights.normal,
},
colorTextSecondary: {
color: theme.colors.textSecondary,
},
colorPrimary: {
color: theme.colors.primary,
},
fontSizeSubheading: {
fontSize: theme.fontSizes.subheading,
},
fontWeightBold: {
fontWeight: theme.fontWeights.bold,
},
});
const Text = ({ color, fontSize, fontWeight, style, ...props }) => {
const textStyle = [
styles.text,
color === 'textSecondary' && styles.colorTextSecondary,
color === 'primary' && styles.colorPrimary,
fontSize === 'subheading' && styles.fontSizeSubheading,
fontWeight === 'bold' && styles.fontWeightBold,
style,
];
return <NativeText style={textStyle} {...props} />;
};
export default Text;
الآن نفّذنا مكوّن النص لدينا. ولهذا المكوّن متغيّرات متسقة من الألوان وأحجام الخطوط وأوزان الخطوط يمكننا استخدامها في أي مكان في تطبيقنا. ويمكننا الحصول على تنويعات نصية مختلفة باستخدام props مختلفة هكذا:
import Text from './Text';
const Main = () => {
return (
<>
<Text>Simple text</Text>
<Text style={{ paddingBottom: 10 }}>Text with custom style</Text>
<Text fontWeight="bold" fontSize="subheading">
Bold subheading
</Text>
<Text color="textSecondary">Text with secondary color</Text>
</>
);
};
export default Main;
لا تتردد في توسيع هذا المكوّن أو تعديله إن رغبت في ذلك. وقد تكون فكرة جيدة أيضاً إنشاء مكوّنات نصية قابلة لإعادة الاستخدام مثل Subheading تستخدم مكوّن Text. كذلك واصل توسيع إعداد الثيم وتعديله مع تقدّم تطبيقك.
استخدام flexbox للتخطيط
المفهوم الأخير الذي سنتناوله فيما يتعلق بالأنماط هو تنفيذ التخطيطات باستخدام flexbox. ويعرف من هم أكثر إلماماً بـCSS أن flexbox لا يتعلق بـReact Native فقط، فله حالات استخدام كثيرة في تطوير الويب أيضاً. ومن يعرف كيف يعمل flexbox في تطوير الويب لن يتعلم كثيراً من هذا القسم على الأرجح. ومع ذلك، لنتعلم أساسيات flexbox أو نراجعها.
flexbox هو كيان تخطيط يتألف من جزأين منفصلين: حاوية flex وداخلها مجموعة من عناصر flex. ولحاوية flex مجموعة من الخصائص التي تتحكم في انسياب عناصرها. ولكي يصبح مكوّن حاوية flex يجب أن تكون خاصية النمط display فيه مضبوطة على flex، وهي القيمة الافتراضية للخاصية display. وإليك مثالاً على حاوية flex:
import { View, StyleSheet } from 'react-native';
const styles = StyleSheet.create({
flexContainer: {
flexDirection: 'row',
},
});
const FlexboxExample = () => {
return <View style={styles.flexContainer}>{/* ... */}</View>;
};
ربما تكون أهم خصائص حاوية flex هي التالية:
- تتحكم خاصية flexDirection في الاتجاه الذي تُرتَّب فيه عناصر flex داخل الحاوية. والقيم الممكنة لهذه الخاصية هي
rowوrow-reverseوcolumn(القيمة الافتراضية) وcolumn-reverse. واتجاه flexrowسيرتّب عناصر flex من اليسار إلى اليمين، بينماcolumnمن الأعلى إلى الأسفل. واتجاهات*-reverseتعكس ببساطة ترتيب عناصر flex. - تتحكم خاصية justifyContent في محاذاة عناصر flex على طول المحور الرئيسي (المحدَّد بخاصية
flexDirection). والقيم الممكنة لهذه الخاصية هيflex-start(القيمة الافتراضية) وflex-endوcenterوspace-betweenوspace-aroundوspace-evenly. - تفعل خاصية alignItems الشيء نفسه الذي تفعله
justifyContentلكن على المحور المقابل. والقيم الممكنة لهذه الخاصية هيflex-startوflex-endوcenterوbaselineوstretch(القيمة الافتراضية).
لننتقل إلى عناصر flex. وكما ذُكر، يمكن أن تحتوي حاوية flex على عنصر flex واحد أو عدة عناصر. ولعناصر flex خصائص تتحكم في كيفية تصرفها بالنسبة إلى عناصر flex الأخرى في الحاوية نفسها. ولكي تجعل مكوّناً ما عنصر flex، كل ما عليك فعله هو جعله ابناً مباشراً لحاوية flex:
import { View, Text, StyleSheet } from 'react-native';
const styles = StyleSheet.create({
flexContainer: {
display: 'flex',
},
flexItemA: {
flexGrow: 0,
backgroundColor: 'green',
},
flexItemB: {
flexGrow: 1,
backgroundColor: 'blue',
},
});
const FlexboxExample = () => {
return (
<View style={styles.flexContainer}>
<View style={styles.flexItemA}>
<Text>Flex item A</Text>
</View>
<View style={styles.flexItemB}>
<Text>Flex item B</Text>
</View>
</View>
);
};
من أكثر خصائص عناصر flex استخداماً خاصية flexGrow. وهي تقبل قيمة بلا وحدة تحدّد قدرة عنصر flex على التمدد عند الحاجة. وإذا كانت قيمة flexGrow لجميع عناصر flex تساوي 1، فسيتقاسمون كل المساحة المتاحة بالتساوي. وإذا كانت قيمة flexGrow لعنصر flex تساوي 0، فسيستخدم فقط المساحة التي يتطلبها محتواه ويترك بقية المساحة لعناصر flex الأخرى.
هنا يمكنك أن تجد كيفية تبسيط التخطيطات باستخدام Flexbox gap: Flexbox gap.
بعد ذلك، اقرأ مقال دليل شامل إلى Flexbox الذي يتضمن أمثلة بصرية شاملة عن flexbox. ومن الجيد أيضاً التجربة مع خصائص flexbox في Flexbox Playground لترى كيف تؤثر خصائص flexbox المختلفة في التخطيط. وتذكّر أنه في React Native تكون أسماء الخصائص هي نفسها كما في CSS باستثناء تسمية camelCase. أما قيم الخصائص مثل flex-start وspace-between فهي متطابقة تماماً.
ملاحظة: هناك بعض الفروق بين React Native وCSS فيما يخص flexbox. وأهم فرق أن القيمة الافتراضية لخاصية flexDirection في React Native هي column. ومن الجدير بالذكر أيضاً أن اختصار flex لا يقبل قيماً متعددة في React Native. ويمكن قراءة المزيد عن تنفيذ flexbox في React Native في الوثائق.
4. شريط التطبيق
5. قائمة المستودعات المُقيَّمة المصقولة
نمط شريط الحالة
اخترنا لون خلفية داكناً لمكوّن AppBar. والمشكلة الآن أن أيقونات شريط الحالة — مثل الساعة وحالة البطارية — لا تبرز جيداً:

ثبّتنا مكتبة expo-status-bar بالفعل عند إعداد Expo سابقاً. والمشكلة سهلة الإصلاح بإضافة مكوّن StatusBar إلى ملف App.js:
import { StatusBar } from 'expo-status-bar';
import Main from './src/components/Main';
const App = () => {
return (
<>
<StatusBar style="light" />
<Main />
</>
);
};
export default App;
يخبر مكوّن StatusBar نظام التشغيل بكيفية عرض شريط الحالة. وبضبط نمطه على light، تصبح أيقونات شريط الحالة أسهل رؤية على خلفية داكنة:

التوجيه
عندما نبدأ بتوسيع تطبيقنا سنحتاج إلى طريقة للانتقال بين عروض مختلفة مثل عرض المستودعات وعرض تسجيل الدخول. في الجزء 5 تعرّفنا على مكتبة React router وتعلمنا كيفية استخدامها لتنفيذ التوجيه في تطبيق ويب.
التوجيه في تطبيق React Native يختلف قليلاً عن التوجيه في تطبيق ويب. والفرق الرئيسي أننا لا نستطيع الإشارة إلى الصفحات بعناوين URL نكتبها في شريط عنوان المتصفح، ولا التنقل ذهاباً وإياباً عبر سجل المستخدم باستخدام history API في المتصفح. غير أن هذه مجرد مسألة تتعلق بواجهة الموجّه التي نستخدمها.
مع React Native يمكننا استخدام نواة React router كاملة، بما في ذلك الخطافات والمكوّنات. والفرق الوحيد عن بيئة المتصفح أن علينا استبدال BrowserRouter بـNativeRouter المتوافق مع React Native، الذي توفّره مكتبة react-router-native. لنبدأ بتثبيت مكتبة react-router-native:
npm install react-router-native
بعد ذلك، افتح ملف App.js وأضف مكوّن NativeRouter إلى مكوّن App:
import { StatusBar } from 'expo-status-bar';
import { NativeRouter } from 'react-router-native';
import Main from './src/components/Main';
const App = () => {
return (
<>
<StatusBar style="auto" />
<NativeRouter>
<Main />
</NativeRouter>
</>
);
};
export default App;
وبعد أن أصبح الموجّه جاهزاً، لنضف مسارنا الأول إلى مكوّن Main في ملف Main.jsx:
import { StyleSheet, View } from 'react-native';
import { Route, Routes, Navigate } from 'react-router-native';
import RepositoryList from './RepositoryList';
import AppBar from './AppBar';
import theme from '../theme';
const styles = StyleSheet.create({
container: {
backgroundColor: theme.colors.mainBackground,
flex: 1,
},
});
const Main = () => {
return (
<View style={styles.container}>
<AppBar />
<Routes>
<Route path="/" element={<RepositoryList />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</View>
);
};
export default Main;
وهذا كل شيء! آخر Route داخل Routes مخصص لالتقاط المسارات التي لا تطابق أي مسار معرّف سابقاً. وفي هذه الحالة، نريد الانتقال إلى العرض الرئيسي.
6. عرض تسجيل الدخول
7. شريط التطبيق القابل للتمرير
إدارة حالة النموذج
الآن بعد أن أصبح لدينا عنصر نائب لعرض تسجيل الدخول، ستكون الخطوة التالية تنفيذ نموذج تسجيل الدخول. وقبل أن نصل إلى ذلك، لنتحدث عن النماذج من منظور أوسع.
يعتمد تنفيذ النماذج اعتماداً كبيراً على إدارة الحالة. وقد يؤدي استخدام خطاف useState في React لإدارة الحالة الغرض في النماذج الصغيرة. غير أنه سيجعل إدارة الحالة للنماذج الأكثر تعقيداً مرهقة بسرعة. ولحسن الحظ هناك مكتبات جيدة كثيرة في منظومة React تسهّل إدارة حالة النماذج. ومن هذه المكتبات Formik.
المفهومان الرئيسيان في Formik هما السياق (context) و الحقل (field). غير أن أسهل طريقة لتنفيذ إرسال نموذج بسيط هي استخدام useFormik(). فهو خطاف مخصص في React يعيد كل حالة Formik والدوال المساعدة مباشرة.
هناك بعض القيود المتعلقة باستخدام UseFormik(). اقرأ هذا لتتعرّف على useFormik().
لنثبّت Formik أولاً:
npm install formik
لنرَ كيف تعمل إدارة الحالة مع Formik بإنشاء نموذج لحساب مؤشر كتلة الجسم:
import { Text, TextInput, Pressable, View } from 'react-native';
import { useFormik } from 'formik';
const initialValues = {
mass: '',
height: '',
};
const getBodyMassIndex = (mass, height) => {
return Math.round(mass / Math.pow(height, 2));
};
const BodyMassIndexForm = ({ onSubmit }) => {
const formik = useFormik({
initialValues,
onSubmit,
});
return (
<View>
<TextInput
placeholder="Weight (kg)"
value={formik.values.mass}
onChangeText={formik.handleChange('mass')}
/>
<TextInput
placeholder="Height (m)"
value={formik.values.height}
onChangeText={formik.handleChange('height')}
/>
<Pressable onPress={formik.handleSubmit}>
<Text>Calculate</Text>
</Pressable>
</View>
);
};
const BodyMassIndexCalculator = () => {
const onSubmit = values => {
const mass = parseFloat(values.mass);
const height = parseFloat(values.height);
if (!isNaN(mass) && !isNaN(height) && height !== 0) {
console.log(`Your body mass index is: ${getBodyMassIndex(mass, height)}`);
}
};
return <BodyMassIndexForm onSubmit={onSubmit} />;
};
export default BodyMassIndexCalculator;
هذا المثال ليس جزءاً من تطبيقنا، لذا لا تحتاج إلى إضافة هذه الشيفرة إلى التطبيق. لكن يمكنك تجربته مثلاً في Expo Snack. وExpo Snack محرّر عبر الإنترنت لـReact Native، مشابه لـJSFiddle وCodePen. وهو منصة مفيدة لتجربة الشيفرة بسرعة. لاحظ أنك تحتاج أيضاً إلى إضافة Formik كاعتمادية في Expo Snack. ويمكنك فعل ذلك بإضافة الاعتمادية مباشرة إلى ملف package.json، مثلاً السطر: "formik": "^2.4.9".
يمكنك مشاركة Expo Snacks مع الآخرين باستخدام رابط أو بتضمينها كـ Snack Player على موقع ويب. وربما صادفت Snack Players مثلاً في هذه المادة وفي وثائق React Native.
8. نموذج تسجيل الدخول
التحقق من صحة النموذج
يوفّر Formik طريقتين للتحقق من صحة النموذج: دالة تحقق أو مخطط تحقق. ودالة التحقق هي دالة تُمرَّر إلى مكوّن Formik كقيمة لخاصية validate. وهي تستقبل قيم النموذج كوسيط وتعيد كائناً يحوي رسائل خطأ محتملة خاصة بكل حقل.
والطريقة الثانية هي مخطط التحقق الذي يُمرَّر إلى مكوّن Formik كقيمة لخاصية validationSchema. ويمكن إنشاء مخطط التحقق هذا باستخدام مكتبة تحقق تُسمّى Yup. لنبدأ بتثبيت Yup:
npm install yup
بعد ذلك، كمثال، لننشئ مخطط تحقق لنموذج مؤشر كتلة الجسم الذي نفّذناه سابقاً. نريد التحقق من أن الحقلين mass وheight موجودان وأنهما رقميان. كذلك ينبغي أن تكون قيمة mass أكبر من أو تساوي 1، وقيمة height أكبر من أو تساوي 0.5. وإليك كيفية تعريف المخطط:
import * as yup from 'yup';
// ...
const validationSchema = yup.object().shape({
mass: yup
.number()
.min(1, 'Weight must be greater or equal to 1')
.required('Weight is required'),
height: yup
.number()
.min(0.5, 'Height must be greater or equal to 0.5')
.required('Height is required'),
});
const BodyMassIndexForm = ({ onSubmit }) => {
const formik = useFormik({
initialValues,
validationSchema,
onSubmit,
});
return (
<View>
<TextInput
placeholder="Weight (kg)"
value={formik.values.mass}
onChangeText={formik.handleChange('mass')}
onBlur={formik.handleBlur('mass')}
/>
{formik.touched.mass && formik.errors.mass && (
<Text style={{ color: 'red' }}>{formik.errors.mass}</Text>
)}
<TextInput
placeholder="Height (m)"
value={formik.values.height}
onChangeText={formik.handleChange('height')}
onBlur={formik.handleBlur('height')}
/>
{formik.touched.height && formik.errors.height && (
<Text style={{ color: 'red' }}>{formik.errors.height}</Text>
)}
<Pressable onPress={formik.handleSubmit}>
<Text>Calculate</Text>
</Pressable>
</View>
);
};
const BodyMassIndexCalculator = () => {
// ...
}
كن على علم بأنك تحتاج إلى تضمين مكوّنات Text هذه داخل View الذي يعيده النموذج لعرض أخطاء التحقق:
{formik.touched.mass && formik.errors.mass && (
<Text style={{ color: 'red' }}>{formik.errors.mass}</Text>
)}
{formik.touched.height && formik.errors.height && (
<Text style={{ color: 'red' }}>{formik.errors.height}</Text>
)}
يُجرى التحقق افتراضياً في كل مرة تتغير فيها قيمة حقل وعند استدعاء الدالة handleSubmit. وإذا فشل التحقق، فلا تُستدعى الدالة المُمرَّرة لخاصية onSubmit في مكوّن Formik.
9. التحقق من صحة نموذج تسجيل الدخول
الشيفرة الخاصة بالمنصة
من المزايا الكبيرة في React Native أننا لا نحتاج إلى القلق بشأن ما إذا كان التطبيق يعمل على جهاز Android أو iOS. غير أنه قد تكون هناك حالات نحتاج فيها إلى تنفيذ شيفرة خاصة بالمنصة. ومثل هذه الحالات قد تكون مثلاً استخدام تنفيذ مختلف لمكوّن على منصة مختلفة.
يمكننا الوصول إلى منصة المستخدم عبر الثابت Platform.OS:
import { Platform, Text, StyleSheet } from 'react-native';
const styles = StyleSheet.create({
text: {
color: Platform.OS === 'android' ? 'green' : 'blue',
},
});
const WhatIsMyPlatform = () => {
return <Text style={styles.text}>Your platform is: {Platform.OS}</Text>;
};
القيم الممكنة لثوابت Platform.OS هي android وios. وهناك طريقة مفيدة أخرى لتعريف فروع شيفرة خاصة بالمنصة هي استخدام الدالة Platform.select. فعند تمرير كائن تكون مفاتيحه واحداً من ios أو android أو native أو default، تعيد الدالة Platform.select القيمة الأنسب للمنصة التي يعمل عليها المستخدم حالياً. ويمكننا إعادة كتابة المتغير styles في المثال السابق باستخدام الدالة Platform.select هكذا:
const styles = StyleSheet.create({
text: {
color: Platform.select({
android: 'green',
ios: 'blue',
default: 'black',
}),
},
});
يمكننا حتى استخدام الدالة Platform.select لاستدعاء مكوّن خاص بمنصة عبر require:
const MyComponent = Platform.select({
ios: () => require('./MyIOSComponent'),
android: () => require('./MyAndroidComponent'),
})();
<MyComponent />;
غير أن هناك طريقة أكثر تطوراً لتنفيذ واستيراد مكوّنات خاصة بالمنصة (أو أي قطعة شيفرة أخرى) وهي استخدام امتدادي الملفين .ios.jsx و .android.jsx. لاحظ أن الامتداد .jsx يمكن أن يكون أيضاً امتداداً آخر يتعرّف عليه المُجمِّع، مثل .js. فيمكن أن تكون لدينا مثلاً الملفان Button.ios.jsx و Button.android.jsx اللذان يمكننا استيرادهما هكذا:
import Button from './Button';
const PlatformSpecificButton = () => {
return <Button />;
};
الآن، ستحتوي حزمة Android للتطبيق على المكوّن المعرّف في Button.android.jsx، بينما تحتوي حزمة iOS على المكوّن المعرّف في ملف Button.ios.jsx.
10. خط خاص بالمنصة