Expo Modules API הוא המסגרת הרשמית של Expo לכתיבת מודולים native חדשים ב-React Native עם Kotlin ל-Android ו-Swift ל-iOS, כולל Sweet API הצהרתי, טיפוסים בטוחים ב-TypeScript ותאימות מלאה ל-New Architecture (Fabric ו-TurboModules). בפועל, זו הדרך המומלצת ב-2026 לחשוף פונקציונליות של פלטפורמה (שיתוף BLE, ניהול קבצים, Keychain ואפילו תצוגות native), בלי לגעת ב-NativeModule הישן, ב-@ReactMethod או ב-Objective-C. במאמר הזה אני מפרקת את הזרימה מקצה לקצה: יצירה, סקימת פונקציות, מודולי View, codegen וקישור למונוריפו של פינטק שאני מריצה כל יום.
מודולים חדשים תאימים מעצם הגדרתם ל-New Architecture (Bridgeless mode ב-React Native 0.76+), בלי קונפיגורציה נוספת.
create-expo-module מייצר scaffold מלא ב-30 שניות, כולל TypeScript, iOS, Android, unit tests ו-example app.
הטיפוסים ב-TypeScript מוגדרים בפרויקט המודול עצמו ומסונכרנים ידנית (אין codegen אוטומטי כמו ב-TurboModules JSI), אבל יש כלי fallback מובנה.
מודולים פועלים גם ב-Expo Managed Workflow עם Dev Client, גם ב-Bare React Native וגם ב-Continuous Native Generation (CNG) דרך prebuild.
לתמיכה ב-C++/JSI raw (כמו MMKV) עדיין תזדקקו ל-Nitro Modules. Expo Modules היא השכבה שמעליהם, לא מתחרה.
מה זה Expo Modules API ולמי הוא מיועד?
Expo Modules API היא ספריית מאקרוזים ו-runtime שמאפשרת להצהיר על מודול native ב-Kotlin או ב-Swift באמצעות בלוק Module { … } יחיד. השכבה הזאת יושבת על גבי expo-modules-core, שמכיל את כל התשתית של רישום המודול, המרת טיפוסים, ניהול חוטים והתחברות ל-JSI. במקום להוריש מ-ReactContextBaseJavaModule, לרשום את המודול ב-package ולסנכרן פרוטוקולים ב-Objective-C, אתם כותבים בלוק אחד שמתאר את כל הפונקציות, הקבועים והאירועים. פשוט וקצר.
המסגרת הזאת יועדה לשלוש קבוצות מפתחים. הראשונה: מפתחי אפליקציות שצריכים לעטוף SDK native ספציפי (למשל SDK של סליקה או אימות ביומטרי בפינטק), בלי לרדת ל-boilerplate של TurboModules. השנייה: מחזיקי ספריות open-source. נכון ל-2026, יותר מ-60% מהספריות בטבלת reactnative.directory שתויגו כתומכות ב-New Architecture משתמשות ב-Expo Modules API. השלישית: צוותי פלטפורמה במונוריפו שרוצים לחשוף שירותי native פנימיים (analytics, feature flags, secure storage) כמודול אחיד לכל האפליקציות של החברה. בשלושת המקרים היתרון זהה: פחות קוד לתחזק, טיפוסים חזקים ותאימות אוטומטית ל-הארכיטקטורה החדשה של React Native.
Expo Modules API מול TurboModules: מה ההבדל?
השאלה הראשונה שאני מקבלת בכל platform review היא "אם TurboModules הם הסטנדרט החדש, למה לא לכתוב אותם ישירות?". התשובה הקצרה: אפשר, אבל תשלמו עליה ב-boilerplate ובזמן תחזוקה. TurboModules הם מפרט low-level שדורש כתיבת spec ב-TypeScript, הרצת codegen שמייצר C++/Java/Objective-C headers, ואז מימוש בפלטפורמה. Expo Modules API הוא abstraction גבוה יותר שמייצר את אותו TurboModule מתחת למכסה המנוע, בלי שתצטרכו לכתוב את ה-spec או להריץ codegen ידני.
קריטריון
Expo Modules API
TurboModules ישיר
שפת מימוש
Kotlin / Swift בלבד
Java / Kotlin + Obj-C / Swift + C++ optional
boilerplate למודול פשוט
~30 שורות
~150 שורות + spec + codegen
Codegen ידני
לא נדרש
נדרש בכל שינוי API
גישה ישירה ל-JSI (C++)
לא
כן
תמיכה ב-Bridgeless mode
אוטומטית
נדרשת קונפיגורציה
תמיכה ב-Expo Prebuild / CNG
מלאה
ידנית דרך config plugins
עקומת למידה
נמוכה
גבוהה
אז מה המסקנה המעשית? אם אתם צריכים להעביר מיליוני קריאות בשנייה דרך שכבת ה-native (למשל serialization של buffer בינארי בקידוד וידאו), רדו ישירות ל-TurboModules או ל-Nitro Modules. לכל שאר המקרים, וזה כולל בערך 95% ממה שאני רואה בפרויקטים אמיתיים, Expo Modules API יחסוך לכם שבועות של עבודה בלי לוותר על ביצועים.
יצירת מודול חדש עם create-expo-module
הדרך המומלצת להתחיל היא npx create-expo-module@latest. הפקודה שואלת על שם המודול, מרחב שמות ל-native package, ומייצרת מונוריפו קטן עם workspace של pnpm/yarn שמכיל את הספרייה, סוגי TypeScript, מימוש iOS ו-Android, unit tests ו-example app מלאה שמפעילה את המודול על מכשיר. ב-2026 ה-CLI מייצר כברירת מחדל תאימות ל-Kotlin 2.0, Swift 5.10, Android Gradle Plugin 8.7 ו-Xcode 16.
# יצירת ספרייה חדשה, למשל אינטגרציה עם ספריית ביומטריה של הבנק
npx create-expo-module@latest expo-bank-biometrics
# ניווט לתיקיית המודול, התקנת תלויות
cd expo-bank-biometrics
pnpm install
# הפעלת example app על iOS (יבנה את המודול, יקשר ויפתח simulator)
cd example
npx expo run:ios
המבנה שהתקבל נראה כך:
expo-bank-biometrics/
├─ src/
│ ├─ index.ts // API ציבורי ב-TypeScript
│ ├─ ExpoBankBiometrics.types.ts
│ └─ ExpoBankBiometricsModule.ts
├─ ios/
│ └─ ExpoBankBiometricsModule.swift
├─ android/
│ └─ src/main/java/.../ExpoBankBiometricsModule.kt
├─ expo-module.config.json
└─ example/ // אפליקציית דמו לצורכי פיתוח
שימו לב לקובץ expo-module.config.json. זה המניפסט שאומר ל-autolinking איזה מודולים לרשום, לאילו פלטפורמות ובאיזה שם. כל שינוי כאן דורש pod install חוזר ב-iOS ו-Gradle sync ב-Android, אבל לא rebuild של האפליקציה כולה.
Sweet API: פונקציות סינכרוניות ואסינכרוניות
הליבה של Sweet API היא בלוק Module שמכריז על כל המבנה של המודול. כל Function, AsyncFunction, Constants, Events ו-Property מתורגמים אוטומטית לחתימת TurboModule המתאימה. Sweet API מטפל בעצמו במעבר בין ה-JS thread לחוט הרקע: Function רץ סינכרונית על ה-JS thread ומחזיר ערך מייד, בעוד ש-AsyncFunction רץ על thread pool ומחזיר Promise. הפרדה נקייה, בלי לחשוב על executors ידנית.
// android/src/main/java/expo/modules/bankbiometrics/ExpoBankBiometricsModule.kt
package expo.modules.bankbiometrics
import androidx.biometric.BiometricPrompt
import androidx.fragment.app.FragmentActivity
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import expo.modules.kotlin.exception.CodedException
class BiometryUnavailable : CodedException("Biometric hardware unavailable")
class ExpoBankBiometricsModule : Module() {
override fun definition() = ModuleDefinition {
Name("ExpoBankBiometrics")
// קבוע נחשף כשדה סטטי ב-JS
Constants(
"isSupported" to isHardwarePresent()
)
// פונקציה סינכרונית, טובה לבדיקות זולות בלבד
Function("getSupportedTypes") {
listOf("fingerprint", "face")
}
// פונקציה אסינכרונית, כאן קוראים ל-BiometricPrompt
AsyncFunction("authenticate") { reason: String, promise: expo.modules.kotlin.Promise ->
val activity = appContext.currentActivity as? FragmentActivity
?: throw CodedException("No activity")
val prompt = BiometricPrompt(
activity,
activity.mainExecutor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
promise.resolve(true)
}
override fun onAuthenticationError(code: Int, msg: CharSequence) {
promise.reject(CodedException("BIOMETRY_$code", msg.toString(), null))
}
}
)
val info = BiometricPrompt.PromptInfo.Builder()
.setTitle("אימות")
.setSubtitle(reason)
.setNegativeButtonText("ביטול")
.build()
prompt.authenticate(info)
}
// אירועים יורדים ל-JS דרך EventEmitter
Events("onAuthChanged")
}
private fun isHardwarePresent(): Boolean = /* … */ true
}
מהצד של iOS זה כמעט זהה, רק ב-Swift. מה שחשוב לשים לב אליו זה שכל הטיפוסים בפרמטרים (String, Int, מפה, מערך של אובייקטים מותאמים אישית) עוברים המרה אוטומטית בין JS לפלטפורמה, כולל data class ב-Kotlin ו-struct ב-Swift עם Record. זה מבטל את הצורך ב-ReadableMap.getString ידני שהיה סבל אין-סופי במודולים הישנים. אני נתקלתי בבאג הזה בדיוק בהעברת אפליקציית פינטק ב-2025, ומאז הקוד היה קצר בערך בחצי.
מודולי View: חשיפת קומפוננטות native
Expo Modules API לא מוגבל לפונקציות. הוא תומך גם ב-View modules שמאפשרים לחשוף UIView או android.view.View כקומפוננטת React מלאה עם props, callbacks ו-imperative methods. הזרימה זהה: מגדירים View(SomeView::class) { … } בתוך אותו בלוק Module, מכריזים על ה-props עם Prop("color"), על אירועים עם Events, ומקבלים אוטומטית קומפוננטת React עם טיפוסי TypeScript מלאים.
// iOS Swift, קומפוננטת מפה מותאמת
import ExpoModulesCore
import MapKit
class MapContainerView: ExpoView {
let mapView = MKMapView()
let onRegionChange = EventDispatcher()
required init(appContext: AppContext? = nil) {
super.init(appContext: appContext)
addSubview(mapView)
mapView.delegate = self
}
}
public class ExpoBankMapModule: Module {
public func definition() -> ModuleDefinition {
Name("ExpoBankMap")
View(MapContainerView.self) {
Prop("initialZoom") { (view: MapContainerView, zoom: Double) in
view.mapView.camera.altitude = zoom
}
Events("onRegionChange")
}
}
}
בצד ה-JS, אתם מקבלים קומפוננטה שנראית ומתנהגת בדיוק כמו כל View אחר של React Native, עם autocompletion מלא של props ב-VS Code. הבעיה הידועה כאן היא שינויי ה-API של Fabric בגרסאות Expo SDK. לפני SDK 51 היה צריך לכתוב wrapper ידני ל-ExpoView, ומ-SDK 52 ואילך זה נעשה אוטומטית. בקוד ישן שאני מהגרת אני תמיד עוברת דרך CHANGELOG כדי לוודא שהגרסה של expo-modules-core תואמת (אחרת אתם מגלים את זה רק ב-runtime, לצערי).
טיפוסים בטוחים ב-TypeScript ו-codegen
בניגוד ל-TurboModules הרגילים ש-codegen מייצר עבורם את הטיפוסים מתוך NativeModule.ts spec, ב-Expo Modules API כותבים את הטיפוסים ב-src/ באופן ידני. זה נראה כמו חיסרון, אבל בפועל זה נותן שליטה מלאה: אפשר להגדיר union types, generics, discriminated unions ו-as const tuples, דברים ש-codegen של React Native היה מסרב לתמוך בהם.
הטיפוסים חיים בפרויקט המודול, נבדקים במידה שווה על ידי הצרכן (בזכות declaration files) ומעודכנים ידנית כשמשנים את חתימת הפונקציה ב-native. בפרויקטים גדולים אני מוסיפה סקריפט pnpm typecheck ב-CI שמריץ tsc --noEmit על ה-example app אחרי כל שינוי במודול. זה תופס בזמן ריצת ה-CI כל drift בין JS ל-native, וחסך לי כבר יותר מכמה pull requests שבורים.
שילוב במונוריפו וב-Bare React Native
אצלנו בפינטק, כל המודולים ה-native שאני כותבת יושבים ב-packages/native-* של מונוריפו pnpm שמכיל שלוש אפליקציות. Expo Modules autolinking עובד out-of-the-box עם pnpm workspaces, כל עוד יש expo-module.config.json בשורש המודול והוא מפורסם ב-package.json תחת files. חשוב לזכור: pnpm יוצר תלויות nested, אז ודאו ש-node_modules/.pnpm/ נגיש לתהליך ה-autolinking על ידי public-hoist-pattern[]=*expo* ב-.npmrc.
ב-Bare React Native (בלי Expo SDK), הזרימה פשוטה: להתקין expo ו-expo-modules-core, להריץ npx install-expo-modules פעם אחת (הפקודה עורכת את Podfile, MainApplication.kt ו-build.gradle), ומאותו רגע כל מודול Expo שיותקן יעבוד. זה כולל גם מודולים שכתבתם בעצמכם. אין הבדל בין ספרייה חיצונית לספרייה פנימית מבחינת ה-runtime. מי שרוצה להעמיק בשילוב פרויקטים מעורבים יכול לעבור על התיעוד הרשמי של Expo Modules Overview ועל מקור expo-modules-core ב-GitHub.
נקודה אחרונה למונוריפו של פינטק: אם המודול חשוף לפעולות רגישות (Keychain, ביומטריה, סליקה), הפרידו את ה-JS layer מה-native layer לשני packages. זה מאפשר להריץ audit של הקוד ה-native ב-CI נפרד עם reviewers שונים, בלי שכל שינוי ב-TS יגרור review שלם של native. אותו עיקרון של הפרדה הצגתי במאמר על Expo Push Notifications עם FCM v1, שם המודול הזה מתאם בעצמו את הטוקנים בין iOS ל-Android.
שאלות נפוצות
האם Expo Modules API עובד ב-Bare React Native project?
כן. מתקינים את expo ו-expo-modules-core ב-package.json ומריצים npx install-expo-modules. הפקודה משנה אוטומטית את קבצי ה-native (Podfile, MainApplication, build.gradle) ומאפשרת autolinking של כל מודול Expo, כולל מודולים שכתבתם בעצמכם. אין צורך ב-Expo SDK המלא.
מה ההבדל בין Expo Modules API ל-Nitro Modules?
Expo Modules API הוא abstraction ברמת Kotlin/Swift שמייצר TurboModule סטנדרטי, מתאים ל-99% ממקרי השימוש. Nitro Modules הן מסגרת ברמת C++/JSI לביצועים קיצוניים (מיליוני קריאות בשנייה, buffer בינארי). ברוב הפרויקטים משלבים את השניים: Expo Modules ל-API כללי, Nitro ל-hot paths בלבד.
האם צריך להריץ codegen ידנית בכל שינוי?
לא. Sweet API של Expo Modules מייצר את ה-TurboModule spec מתחת למכסה המנוע בזמן build. הדבר היחיד שצריך לעדכן ידנית זה קובצי הטיפוסים ב-src/ של TypeScript. אלה לא נוצרים אוטומטית ומאפשרים חופש מלא ב-union types ו-generics.
איך מדבגים מודול native שנכתב ב-Expo Modules API?
בדיוק כמו כל קוד native: Xcode עבור iOS ו-Android Studio עבור Android. ה-example app שנוצרת אוטומטית על ידי create-expo-module היא היעד לדיבאג. מריצים npx expo run:ios --device, פותחים את example/ios/*.xcworkspace ב-Xcode ומקבלים breakpoints מלאים ב-Swift. ל-JS bridge משתמשים ב-console.log או ב-Hermes debugger הרגיל.
האם Expo Modules API תואם ל-New Architecture ול-Bridgeless mode?
כן, מלאה ואוטומטית. כל מודול שנכתב עם Expo Modules API רץ כ-TurboModule בתוך Bridgeless mode של React Native 0.76+. אין קונפיגורציה נדרשת מצד המפתח: expo-modules-core מזהה את הרנטיים ורושם את המודול בהתאם.
מדריך מקיף 2026 להגדרת Expo Push Notifications ב-React Native: יצירת Development Build, חיבור FCM v1 דרך Service Account, הגדרת APNs ל-iOS, ניהול Notification Channels וטיפול ב-Tickets ו-Receipts בפרודקשן.