React Native עם Expo
מה זה React Native?
React Native הוא פריימוורק של Meta (Facebook) לפיתוח אפליקציות מובייל Native לאנדרואיד ול-iOS, באמצעות אותם JavaScript ו-React שכבר הכרנו. היתרון הגדול: במקום ללמוד שפות שונות ולתחזק שתי אפליקציות נפרדות, כותבים בסיס קוד אחד שמתורגם לרכיבי ממשק מקוריים בכל פלטפורמה - לא אתר עטוף (WebView), אלא אפליקציה אמיתית לכל דבר.
React (Web) → HTML Elements → דפדפן
React Native (Mobile) → Native Components → iOS / Android
יתרונות React Native
| יתרון | הסבר |
|---|---|
| קוד אחד | אותו קוד לשתי הפלטפורמות |
| React | אותו תחביר ועקרונות מ-React |
| Native | UI אמיתי, לא WebView |
| Hot Reload | רענון מהיר בפיתוח |
| קהילה גדולה | הרבה ספריות וכלים |
| ביצועים | קרוב ל-Native אמיתי |
מה זה Expo?
Expo הוא מסגרת עבודה ומערכת כלים שנבנו סביב React Native כדי לפשט מאוד את הפיתוח. במקום להתמודד עם הגדרת סביבות מורכבות ב-Android Studio וב-Xcode, Expo מטפל בכל התשתית עבורכם - כך שאפשר להתחיל לפתח ולראות תוצאות על מכשיר אמיתי תוך דקות. זו נקודת הפתיחה המומלצת ביותר למי שמתחיל עם React Native.
השוואה: Expo vs React Native CLI
Expo React Native CLI
==== ================
התקנה: קלה מאוד מורכבת
זמן הקמה: דקות שעות
דרישות: Node.js בלבד Xcode, Android Studio
Native Modules: מוגבל* מלא
גודל APK: גדול יותר קטן יותר
למתחילים: מומלץ מאוד מאתגר
* Expo Go מוגבל, אבל EAS Build מאפשר הכל
סוגי Workflows ב-Expo
1. Managed Workflow (מנוהל)
- Expo מנהל את הכל
- אין גישה לקוד Native
- הכי פשוט למתחילים
2. Bare Workflow (חשוף)
- גישה מלאה לקוד Native
- יותר שליטה
- דומה ל-React Native CLI
התקנה והגדרה
1. התקנת Node.js
# בדיקה שיש Node.js
node --version # צריך 18+
npm --version
2. התקנת Expo CLI
# אין צורך להתקין גלובלית יותר, משתמשים ב-npx
npx create-expo-app@latest
3. יצירת פרויקט חדש
# יצירת פרויקט
npx create-expo-app@latest my-app
# או עם template
npx create-expo-app@latest my-app --template blank
npx create-expo-app@latest my-app --template blank-typescript
npx create-expo-app@latest my-app --template tabs
# כניסה לפרויקט
cd my-app
4. הרצת הפרויקט
# הרצה
npx expo start
# או
npm start
5. צפייה באפליקציה
- Expo Go App: הורידו מה-App Store או Google Play
- סרקו את ה-QR Code מהטרמינל
- או לחצו
aלאנדרואיד /iל-iOS (דורש אמולטור)
מבנה פרויקט Expo
my-app/
├── app/ # App Router (Expo Router)
│ ├── (tabs)/ # קבוצת טאבים
│ │ ├── index.tsx # מסך ראשי
│ │ └── explore.tsx # מסך נוסף
│ ├── _layout.tsx # Layout ראשי
│ └── +not-found.tsx # דף 404
├── assets/ # תמונות, פונטים, וכו'
│ ├── images/
│ └── fonts/
├── components/ # רכיבים משותפים
├── constants/ # קבועים
├── hooks/ # Custom Hooks
├── app.json # הגדרות האפליקציה
├── package.json # תלויות
├── tsconfig.json # הגדרות TypeScript
└── babel.config.js # הגדרות Babel
רכיבים בסיסיים
ב-React Native אין תגיות HTML כמו div או p. במקומן משתמשים ברכיבים מובנים שמתורגמים לרכיבי ממשק מקוריים. אלו אבני הבניין שתפגשו כמעט בכל מסך:
View - הקונטיינר הבסיסי (כמו div)
import { View, StyleSheet } from 'react-native';
export default function MyComponent() {
return (
<View style={styles.container}>
{/* תוכן */}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
padding: 20,
},
});
Text - טקסט (חובה לכל טקסט!)
import { Text, View } from 'react-native';
export default function TextExample() {
return (
<View>
{/* ❌ לא עובד ב-React Native */}
{/* <View>שלום עולם</View> */}
{/* ✅ נכון */}
<Text>שלום עולם!</Text>
<Text style={{ fontSize: 24, fontWeight: 'bold', color: 'blue' }}>
כותרת גדולה
</Text>
{/* Text מקונן */}
<Text>
טקסט רגיל <Text style={{ fontWeight: 'bold' }}>מודגש</Text> ועוד
</Text>
</View>
);
}
Image - תמונות
import { Image, View } from 'react-native';
export default function ImageExample() {
return (
<View>
{/* תמונה מקומית */}
<Image
source={require('./assets/logo.png')}
style={{ width: 100, height: 100 }}
/>
{/* תמונה מה-URL */}
<Image
source={{ uri: 'https://example.com/image.jpg' }}
style={{ width: 200, height: 200 }}
resizeMode="cover" // cover, contain, stretch, center
/>
</View>
);
}
Button ו-Pressable
import { Button, Pressable, Text, View, Alert } from 'react-native';
export default function ButtonExample() {
const handlePress = () => {
Alert.alert('לחצת!', 'הכפתור עובד');
};
return (
<View>
{/* Button בסיסי (מוגבל בסגנון) */}
<Button
title="לחץ כאן"
onPress={handlePress}
color="#841584"
/>
{/* Pressable - יותר גמיש */}
<Pressable
onPress={handlePress}
style={({ pressed }) => [
{
backgroundColor: pressed ? '#ddd' : '#007AFF',
padding: 15,
borderRadius: 10,
marginTop: 10,
}
]}
>
<Text style={{ color: 'white', textAlign: 'center' }}>
כפתור מותאם
</Text>
</Pressable>
</View>
);
}
TextInput - שדה קלט
import { useState } from 'react';
import { TextInput, View, Text, StyleSheet } from 'react-native';
export default function InputExample() {
const [text, setText] = useState('');
const [password, setPassword] = useState('');
return (
<View style={styles.container}>
{/* שדה טקסט רגיל */}
<TextInput
style={styles.input}
placeholder="הכנס שם"
value={text}
onChangeText={setText}
/>
{/* שדה סיסמה */}
<TextInput
style={styles.input}
placeholder="סיסמה"
value={password}
onChangeText={setPassword}
secureTextEntry={true}
/>
{/* שדה מספרי */}
<TextInput
style={styles.input}
placeholder="מספר טלפון"
keyboardType="phone-pad"
/>
{/* שדה אימייל */}
<TextInput
style={styles.input}
placeholder="אימייל"
keyboardType="email-address"
autoCapitalize="none"
/>
<Text>הקלדת: {text}</Text>
</View>
);
}
const styles = StyleSheet.create({
container: { padding: 20 },
input: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
padding: 12,
marginBottom: 10,
fontSize: 16,
},
});
ScrollView - גלילה
import { ScrollView, View, Text, StyleSheet } from 'react-native';
export default function ScrollExample() {
return (
<ScrollView style={styles.container}>
{[...Array(20)].map((_, i) => (
<View key={i} style={styles.item}>
<Text>פריט מספר {i + 1}</Text>
</View>
))}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
item: {
padding: 20,
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
});
FlatList - רשימה (יעיל לרשימות ארוכות!)
import { FlatList, View, Text, StyleSheet } from 'react-native';
const DATA = [
{ id: '1', title: 'פריט ראשון' },
{ id: '2', title: 'פריט שני' },
{ id: '3', title: 'פריט שלישי' },
// ... עוד הרבה פריטים
];
export default function ListExample() {
const renderItem = ({ item }) => (
<View style={styles.item}>
<Text>{item.title}</Text>
</View>
);
return (
<FlatList
data={DATA}
renderItem={renderItem}
keyExtractor={item => item.id}
ItemSeparatorComponent={() => <View style={styles.separator} />}
ListHeaderComponent={() => <Text style={styles.header}>הרשימה שלי</Text>}
ListEmptyComponent={() => <Text>אין פריטים</Text>}
/>
);
}
const styles = StyleSheet.create({
item: { padding: 20 },
separator: { height: 1, backgroundColor: '#eee' },
header: { fontSize: 24, padding: 20, fontWeight: 'bold' },
});
עיצוב ב-React Native
העיצוב ב-React Native דומה ל-CSS אך אינו זהה לו: כותבים אותו כאובייקטים של JavaScript (בדרך כלל דרך StyleSheet.create), שמות המאפיינים ב-camelCase, והפריסה מבוססת כולה על Flexbox. שימו לב לכמה הבדלים מהותיים: הערכים הם מספרים ולא מחרוזות עם px, ו-flexDirection הוא column כברירת מחדל (בניגוד ל-row בדפדפן):
StyleSheet
import { StyleSheet, View, Text } from 'react-native';
export default function StyledComponent() {
return (
<View style={styles.container}>
<Text style={styles.title}>כותרת</Text>
<Text style={[styles.text, styles.highlight]}>טקסט מודגש</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
padding: 20,
// אין margin: auto, אין display: grid
// משתמשים ב-Flexbox!
},
title: {
fontSize: 28,
fontWeight: 'bold',
color: '#333',
marginBottom: 10,
},
text: {
fontSize: 16,
color: '#666',
lineHeight: 24,
},
highlight: {
backgroundColor: 'yellow',
padding: 5,
},
});
Flexbox (ברירת המחדל!)
const styles = StyleSheet.create({
// עמודה (ברירת מחדל - שונה מ-Web!)
column: {
flexDirection: 'column', // ברירת מחדל
},
// שורה
row: {
flexDirection: 'row',
},
// מרכוז
centered: {
justifyContent: 'center', // ציר ראשי
alignItems: 'center', // ציר משני
},
// חלוקת מרחב
spaceBetween: {
justifyContent: 'space-between',
},
// גמישות
flexible: {
flex: 1, // תופס את כל המקום הזמין
},
});
הבדלים מ-CSS
// ❌ לא עובד ב-React Native
// margin: '10px'
// font-size: 16px
// background-color: red
// ✅ נכון ב-React Native
{
margin: 10, // מספר בלבד (pixels)
fontSize: 16, // camelCase
backgroundColor: 'red',
// ❌ אין:
// display: 'block' / 'inline'
// float
// position: 'fixed'
// ✅ יש:
// position: 'absolute' / 'relative'
// flexbox (ברירת מחדל)
}
ניווט עם Expo Router
Expo Router מיישם ניתוב מבוסס קבצים (File-based routing), בדומה ל-Next.js: מבנה התיקיות בתוך תיקיית app הוא שמגדיר את הנתיבים באפליקציה. כך אין צורך להגדיר נתיבים ידנית - כל קובץ הופך אוטומטית למסך:
התקנה
npx expo install expo-router expo-linking expo-constants expo-status-bar
מבנה תיקיות
app/
├── _layout.tsx # Layout ראשי
├── index.tsx # דף הבית (/)
├── about.tsx # דף אודות (/about)
├── profile/
│ ├── index.tsx # /profile
│ └── [id].tsx # /profile/123 (דינמי)
└── (tabs)/ # קבוצת טאבים
├── _layout.tsx # Layout לטאבים
├── home.tsx # Tab 1
└── settings.tsx # Tab 2
Layout בסיסי
// app/_layout.tsx
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<Stack>
<Stack.Screen
name="index"
options={{ title: 'דף הבית' }}
/>
<Stack.Screen
name="about"
options={{ title: 'אודות' }}
/>
</Stack>
);
}
ניווט בין מסכים
// app/index.tsx
import { Link, router } from 'expo-router';
import { View, Text, Pressable } from 'react-native';
export default function HomeScreen() {
return (
<View>
{/* Link (דומה לאנכור) */}
<Link href="/about">
<Text>עבור לאודות</Text>
</Link>
{/* ניווט פרוגרמטי */}
<Pressable onPress={() => router.push('/profile/123')}>
<Text>עבור לפרופיל</Text>
</Pressable>
{/* חזרה */}
<Pressable onPress={() => router.back()}>
<Text>חזור</Text>
</Pressable>
</View>
);
}
קבלת פרמטרים
// app/profile/[id].tsx
import { useLocalSearchParams } from 'expo-router';
import { View, Text } from 'react-native';
export default function ProfileScreen() {
const { id } = useLocalSearchParams();
return (
<View>
<Text>פרופיל מספר: {id}</Text>
</View>
);
}
Tab Navigation
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
export default function TabLayout() {
return (
<Tabs>
<Tabs.Screen
name="home"
options={{
title: 'בית',
tabBarIcon: ({ color, size }) => (
<Ionicons name="home" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="settings"
options={{
title: 'הגדרות',
tabBarIcon: ({ color, size }) => (
<Ionicons name="settings" size={size} color={color} />
),
}}
/>
</Tabs>
);
}
APIs נפוצים של Expo
אחד היתרונות הגדולים של Expo הוא אוסף המודולים המובנים לגישה ליכולות המכשיר - אחסון, מצלמה, מיקום, התראות ועוד. כל אחד מהם מותקן בפקודה אחת ומספק API נקי ואחיד לשתי הפלטפורמות. הנה כמה מהנפוצים ביותר:
AsyncStorage - אחסון מקומי
npx expo install @react-native-async-storage/async-storage
import AsyncStorage from '@react-native-async-storage/async-storage';
// שמירה
await AsyncStorage.setItem('user', JSON.stringify({ name: 'דוד' }));
// קריאה
const userJson = await AsyncStorage.getItem('user');
const user = userJson ? JSON.parse(userJson) : null;
// מחיקה
await AsyncStorage.removeItem('user');
SecureStore - אחסון מאובטח
npx expo install expo-secure-store
import * as SecureStore from 'expo-secure-store';
// שמירה
await SecureStore.setItemAsync('token', 'abc123');
// קריאה
const token = await SecureStore.getItemAsync('token');
// מחיקה
await SecureStore.deleteItemAsync('token');
Camera - מצלמה
npx expo install expo-camera
import { CameraView, useCameraPermissions } from 'expo-camera';
import { useState } from 'react';
import { View, Button, Text } from 'react-native';
export default function CameraExample() {
const [permission, requestPermission] = useCameraPermissions();
if (!permission) {
return <View />;
}
if (!permission.granted) {
return (
<View>
<Text>צריך הרשאה למצלמה</Text>
<Button onPress={requestPermission} title="תן הרשאה" />
</View>
);
}
return (
<CameraView style={{ flex: 1 }} facing="back">
{/* כפתורי שליטה */}
</CameraView>
);
}
Location - מיקום
npx expo install expo-location
import * as Location from 'expo-location';
import { useEffect, useState } from 'react';
import { View, Text } from 'react-native';
export default function LocationExample() {
const [location, setLocation] = useState(null);
useEffect(() => {
(async () => {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
return;
}
const loc = await Location.getCurrentPositionAsync({});
setLocation(loc);
})();
}, []);
return (
<View>
{location && (
<Text>
מיקום: {location.coords.latitude}, {location.coords.longitude}
</Text>
)}
</View>
);
}
Notifications - התראות
npx expo install expo-notifications expo-device
import * as Notifications from 'expo-notifications';
import { useEffect } from 'react';
// הגדרת התנהגות כשמתקבלת התראה
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: false,
}),
});
export default function NotificationExample() {
useEffect(() => {
registerForPushNotificationsAsync();
}, []);
// שליחת התראה מקומית
const sendLocalNotification = async () => {
await Notifications.scheduleNotificationAsync({
content: {
title: "שלום!",
body: "זו התראה מהאפליקציה",
},
trigger: { seconds: 2 },
});
};
return (/* ... */);
}
בניה והפצה
כשהאפליקציה מוכנה, יש להפוך אותה לקובץ התקנה (APK או IPA) ולהעלות אותה לחנויות. Expo מספק לכך שירות ענן בשם EAS (Expo Application Services), שבונה את האפליקציה עבורכם ללא צורך ב-Xcode או Android Studio מקומיים:
EAS Build - בניית אפליקציה
# התקנת EAS CLI
npm install -g eas-cli
# התחברות
eas login
# הגדרה ראשונית
eas build:configure
# בנייה לאנדרואיד
eas build --platform android
# בנייה ל-iOS
eas build --platform ios
# בנייה לשניהם
eas build --platform all
סוגי Build
# Development build (לפיתוח)
eas build --profile development --platform android
# Preview build (לבדיקות)
eas build --profile preview --platform android
# Production build (להפצה)
eas build --profile production --platform android
EAS Submit - העלאה לחנויות
# העלאה ל-Google Play
eas submit --platform android
# העלאה ל-App Store
eas submit --platform ios
דוגמה מלאה - אפליקציית Todo
// app/index.tsx
import { useState } from 'react';
import {
View,
Text,
TextInput,
FlatList,
Pressable,
StyleSheet,
} from 'react-native';
interface Todo {
id: string;
text: string;
completed: boolean;
}
export default function TodoApp() {
const [todos, setTodos] = useState<Todo[]>([]);
const [inputText, setInputText] = useState('');
const addTodo = () => {
if (inputText.trim()) {
setTodos([
...todos,
{ id: Date.now().toString(), text: inputText, completed: false },
]);
setInputText('');
}
};
const toggleTodo = (id: string) => {
setTodos(
todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
const deleteTodo = (id: string) => {
setTodos(todos.filter((todo) => todo.id !== id));
};
return (
<View style={styles.container}>
<Text style={styles.title}>📝 המשימות שלי</Text>
<View style={styles.inputContainer}>
<TextInput
style={styles.input}
placeholder="הוסף משימה..."
value={inputText}
onChangeText={setInputText}
onSubmitEditing={addTodo}
/>
<Pressable style={styles.addButton} onPress={addTodo}>
<Text style={styles.addButtonText}>+</Text>
</Pressable>
</View>
<FlatList
data={todos}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={styles.todoItem}>
<Pressable
style={styles.checkbox}
onPress={() => toggleTodo(item.id)}
>
<Text>{item.completed ? '✅' : '⬜'}</Text>
</Pressable>
<Text
style={[
styles.todoText,
item.completed && styles.completedText,
]}
>
{item.text}
</Text>
<Pressable onPress={() => deleteTodo(item.id)}>
<Text style={styles.deleteButton}>🗑️</Text>
</Pressable>
</View>
)}
ListEmptyComponent={
<Text style={styles.emptyText}>אין משימות. הוסף אחת!</Text>
}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
padding: 20,
paddingTop: 60,
},
title: {
fontSize: 28,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 20,
},
inputContainer: {
flexDirection: 'row',
marginBottom: 20,
},
input: {
flex: 1,
backgroundColor: 'white',
padding: 15,
borderRadius: 10,
fontSize: 16,
marginRight: 10,
},
addButton: {
backgroundColor: '#007AFF',
width: 50,
borderRadius: 10,
justifyContent: 'center',
alignItems: 'center',
},
addButtonText: {
color: 'white',
fontSize: 24,
fontWeight: 'bold',
},
todoItem: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'white',
padding: 15,
borderRadius: 10,
marginBottom: 10,
},
checkbox: {
marginRight: 10,
},
todoText: {
flex: 1,
fontSize: 16,
},
completedText: {
textDecorationLine: 'line-through',
color: '#888',
},
deleteButton: {
fontSize: 20,
},
emptyText: {
textAlign: 'center',
color: '#888',
marginTop: 50,
},
});
משאבים נוספים
סיכום
| נושא | טכנולוגיה |
|---|---|
| פלטפורמה | Expo |
| ניווט | Expo Router |
| סגנונות | StyleSheet + Flexbox |
| אחסון | AsyncStorage / SecureStore |
| בנייה | EAS Build |
| הפצה | EAS Submit |
בהצלחה! 🎉
בדקו את עצמכם
נסו לענות לבד לפני שאתם פותחים את התשובה.
-
מה ההבדל העיקרי בין React Native ל-React לווב?
הצגת התשובה
תשובה ב. הלוגיקה, ה-Hooks וה-state זהים, רק רכיבי התצוגה שונים.
-
מה Expo מספק?
הצגת התשובה
תשובה ב. Expo היא הדרך המומלצת רשמית להתחיל פרויקט React Native.