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

בהצלחה! 🎉

בדקו את עצמכם

נסו לענות לבד לפני שאתם פותחים את התשובה.

  1. מה ההבדל העיקרי בין React Native ל-React לווב?

    1. React Native רץ בדפדפן
    2. במקום div ו-span משתמשים ברכיבים כמו View ו-Text, שהופכים לרכיבים נייטיב
    3. אין הבדל
    4. אין Hooks ב-React Native
    הצגת התשובה

    תשובה ב. הלוגיקה, ה-Hooks וה-state זהים, רק רכיבי התצוגה שונים.

  2. מה Expo מספק?

    1. מסד נתונים
    2. סביבה מוכנה להרצה על טלפון (Expo Go), build וספריות נייטיב, בלי Xcode או Android Studio בהתחלה
    3. שרת
    4. עיצוב
    הצגת התשובה

    תשובה ב. Expo היא הדרך המומלצת רשמית להתחיל פרויקט React Native.