Expo Modules API 2026: כתיבת מודולים native ב-TypeScript עם Sweet API

המדריך המעשי לכתיבת מודולים native ב-React Native עם Expo Modules API ו-Sweet API ב-2026: יצירה, פונקציות סינכרוניות ואסינכרוניות, מודולי View, טיפוסים בטוחים ב-TypeScript, שילוב במונוריפו pnpm ותאימות אוטומטית ל-New Architecture.

עודכן: 29 באוגוסט, 2026

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 וקישור למונוריפו של פינטק שאני מריצה כל יום.

  • Sweet API: DSL הצהרתי ב-Kotlin ו-Swift שמצמצם boilerplate של TurboModule ב-70% ומטפל אוטומטית ב-thread hopping.
  • מודולים חדשים תאימים מעצם הגדרתם ל-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 APITurboModules ישיר
שפת מימוש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 היה מסרב לתמוך בהם.

// src/ExpoBankBiometrics.types.ts
export type BiometryType = "fingerprint" | "face" | "iris";

export type AuthenticateOptions = {
  reason: string;
  allowDeviceCredential?: boolean;
  fallbackLabel?: string;
};

export type AuthResult =
  | { success: true; type: BiometryType }
  | { success: false; error: "cancelled" | "lockout" | "unavailable" };

// src/index.ts
import { requireNativeModule, EventEmitter } from "expo-modules-core";
import type { AuthenticateOptions, AuthResult, BiometryType } from "./ExpoBankBiometrics.types";

type ExpoBankBiometricsModule = {
  readonly isSupported: boolean;
  getSupportedTypes(): BiometryType[];
  authenticate(opts: AuthenticateOptions): Promise<AuthResult>;
  addListener(event: "onAuthChanged", handler: (p: AuthResult) => void): void;
};

const native = requireNativeModule<ExpoBankBiometricsModule>("ExpoBankBiometrics");
export const BankBiometrics = 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 מזהה את הרנטיים ורושם את המודול בהתאם.

Yelena Petrov
אודות הכותב Yelena Petrov

React Native architect at a fintech. Builds platform teams, type-safe bridges, and runs the upgrade playbook so others don't have to.