expo-calendar permission functions
Permission functions remain: requestCalendarPermissions(), getCalendarPermissions(), requestRemindersPermissions(), getRemindersPermissions(). The Async suffix is dropped. Hooks useCalendarPermissions and useRemindersPermissions are unchanged.
expo-calendar getSourcesSync synchronous
Get calendar sources using getSourcesSync(), which is now synchronous. This replaces the legacy asynchronous getSourcesAsync(). Fetching a single source by ID has no direct equivalent in the new API.
expo-calendar createEvent returns instance not ID
The new calendar.createEvent() method returns an ExpoCalendarEvent instance instead of a string ID. Usage: const event = await calendar.createEvent({ title: 'Lunch', startDate, endDate });
expo-calendar reminder.delete iOS only
Delete a reminder using await reminder.delete(). This replaces the legacy deleteReminderAsync(reminderId). iOS only.
expo-calendar getDefaultCalendarSync iOS only
Get the default calendar on iOS using getDefaultCalendarSync(), which is now synchronous. This replaces the legacy asynchronous getDefaultCalendarAsync().
expo-calendar reminder.update iOS only
Update a reminder using await reminder.update({ title: 'Buy oat milk' }). This replaces the legacy updateReminderAsync(reminderId, options). iOS only.
expo-calendar presentPicker iOS only
Show a calendar picker on iOS using presentPicker(). It returns an ExpoCalendar instance if the user selects one, or null if the user dismisses the picker. This is new in the API.
expo-calendar calendar.listReminders iOS only
List reminders in a calendar using calendar.listReminders(startDate, endDate, status). This replaces the legacy getRemindersAsync([calendarId], status, startDate, endDate). iOS only.
expo-calendar createReminder returns instance not ID
The new calendar.createReminder() method returns an ExpoCalendarReminder instance instead of a string ID. Usage: const reminder = await calendar.createReminder({ title: 'Buy milk' }). iOS only.
expo-calendar attendee.delete Android only
Delete an attendee using await attendee.delete(). This is an instance method and is only supported on Android.
expo-calendar calendar.delete method
Delete a calendar using the instance method: await calendar.delete(). This replaces the legacy deleteCalendarAsync(calendarId).
expo-calendar attendee.update Android only
Update an attendee using await attendee.update({ name: 'Alexander' }). This is an instance method and is only supported on Android.
expo-calendar event.createAttendee method
Create an attendee using const attendee = await event.createAttendee({ email: 'alex@example.com', name: 'Alex' }). Returns an ExpoCalendarAttendee instance. Role, type, and status parameters are no longer required.
expo-calendar calendar.update method
Update a calendar using the instance method: await calendar.update({ title: 'Renamed' }). This replaces the legacy updateCalendarAsync(calendarId, options).
expo-calendar event.getAttendees method
Get attendees of an event using await event.getAttendees(). This replaces the legacy getAttendeesForEventAsync(eventId).
expo-calendar event.getOccurrenceSync method
Get a recurring event occurrence using event.getOccurrenceSync({ instanceStartDate }). First obtain the event instance via ExpoCalendarEvent.get(eventId).
expo-calendar createCalendar returns instance not ID
The new createCalendar() function returns an ExpoCalendar instance instead of a string ID. Usage: const calendar = await createCalendar({ title: 'My Calendar', color: '#ff0000' });
expo-calendar event.editInCalendar and calendar.addEventWithForm
Edit an event using event.editInCalendar(params). To create a new event with form, use calendar.addEventWithForm({ title, startDate, endDate }). The id field is no longer part of params. Presentation options (startNewActivityTask) are now passed in the same params object.
expo-calendar new API class-based imports
The new API exports class instances: import { ExpoCalendar, ExpoCalendarEvent } from 'expo-calendar'. Import additional classes ExpoCalendarReminder and ExpoCalendarAttendee as needed.
expo-calendar event.openInCalendar parameters
Use event.openInCalendar(params) to open an event in calendar. The id field is no longer part of params (it comes from the event instance). Presentation options (allowsEditing, allowsCalendarPreview, startNewActivityTask) are now passed in the same params object instead of as a separate argument.
expo-calendar event.delete always deletes entire series
The new event.delete() method always deletes the entire recurring event series. The legacy recurringEventOptions parameter (iOS only) that allowed targeting single occurrences or future occurrences is not supported.
expo-calendar event.update always modifies entire series
The new event.update() method always modifies the entire recurring event series. The legacy recurringEventOptions parameter (iOS only) that allowed targeting single occurrences or future occurrences is not supported.
expo-calendar Async suffix removed
The Async suffix is dropped from function names in the new API. The majority of the library is asynchronous by default. Only synchronous functions use a Sync suffix (for example, getDefaultCalendarSync, getSourcesSync, getOccurrenceSync).
expo-calendar legacy API import path
The legacy expo-calendar API is available from 'expo-calendar/legacy'. Users can import the legacy API during migration while updating to the new API from the root 'expo-calendar' import.
Listen for media library changes with new API
Use addListener(event => { ... }) to subscribe to library changes, with subscription.remove() to unsubscribe. Use removeAllListeners() to remove all listeners at once. The listener event shape is unchanged from the legacy API.
New expo-media-library API structure
The new class-based expo-media-library API replaces function-based MediaLibrary.getAssetsAsync() with Asset, Album, and Query classes. Albums and assets are represented as class instances that hold only the ID of the native asset. Asset properties are async getters instead of pre-fetched fields. Query replaces getAssetsAsync with a chainable builder pattern.
Delete assets with new API
For a single asset, call asset.delete(). For multiple assets, call Asset.delete([asset1, asset2]). This replaces the legacy MediaLibrary.deleteAssetsAsync([asset]).
Removed functions in new expo-media-library API
The following legacy functions are removed with no replacement: getMomentsAsync, albumNeedsMigrationAsync, and migrateAlbumIfNeededAsync. These calls can be safely deleted.
Breaking changes in new expo-media-library API
Breaking semantic changes: (1) Asset properties are now async getters (getFilename(), getWidth(), etc.) instead of synchronous fields; use asset.getInfo() for all properties at once. (2) The 'Async' suffix is dropped; the entire library is asynchronous. (3) Query replaces getAssetsAsync options bag with no endCursor/hasNextPage; use .limit() and .offset() for pagination. (4) Operations on albums and assets are methods on instances instead of free functions accepting IDs. (5) saveToLibraryAsync is replaced by Asset.create which returns an Asset instance.
Media library permissions in new API
Permission hooks and functions keep the same names. The only change is that presentPermissionsPickerAsync was renamed to presentPermissionsPicker. requestPermissionsAsync, getPermissionsAsync, and usePermissions are unchanged.
Read asset properties with new API
Asset properties are accessed through async getters: asset.getFilename(), asset.getWidth(), asset.getHeight(), asset.getMediaType(). Use asset.getInfo() to retrieve all properties at once as an AssetInfo object. EXIF data is accessed via asset.getExif().
Delete album with new API
For a single album, call await album.delete(). For multiple albums, call await Album.delete([album1, album2]). This replaces MediaLibrary.deleteAlbumsAsync([album], false).
Query assets with new API
Use the Query class with chainable methods: new Query().eq(AssetField.MEDIA_TYPE, MediaType.IMAGE).limit(20).orderBy({ key: AssetField.CREATION_TIME, ascending: false }).exe(). Query returns an array of Asset instances directly with no assets wrapper or endCursor. Pagination uses .limit() and .offset() methods instead of firstCursor/endCursor.
Remove assets from album with new API
Call await album.removeAssets(assets) to remove assets from an album. This operation is only available on iOS. This replaces MediaLibrary.removeAssetsFromAlbumAsync(assets, album).
Add assets to album with new API
Call await album.add([asset]) to add one or more assets to an album. This replaces MediaLibrary.addAssetsToAlbumAsync([asset], album, false).
Get album title with new API
Use await album.getTitle() to get an album's title. In the legacy API, title was a synchronous property that required fetching the full album object first.
Get all assets in album with new API
Call album.getAssets() on an Album instance to get all assets in that album. This replaces MediaLibrary.getAssetsAsync({ album: album.id }).
Create album with new API
Use Album.create('MyNewAlbum', [asset]) where the second argument is an array of assets to add to the album. This replaces MediaLibrary.createAlbumAsync('MyNewAlbum', asset, false).
Get all albums with new API
Use Album.getAll() to retrieve all albums. This replaces MediaLibrary.getAlbumsAsync().
Get album by name with new API
Use Album.get('MyAlbum') which returns the album if found or null if not found. This replaces MediaLibrary.getAlbumAsync('MyAlbum').
Create asset from file with new API
Use Asset.create(localUri) to save a file to the library and get an Asset instance back. This replaces the legacy MediaLibrary.saveToLibraryAsync(localUri) and MediaLibrary.createAssetAsync(localUri). The function saveToLibraryAsync is not available in the new API.
expo-media-library legacy API location
The legacy function-based expo-media-library API is available from the 'expo-media-library/legacy' import path. New code should import from the root 'expo-media-library' to use the new class-based API.
Hermes improves app startup time and reduces binary size
Hermes improves app startup time by compiling JavaScript into bytecode ahead of time. The binary size of Hermes is smaller than other JavaScript engines such as JavaScriptCore (JSC), and it uses less memory at runtime, which is particularly valuable on lower-end Android devices.
Setting jsEngine in app.json to switch JavaScript engines per platform
Set the "jsEngine" field at the top level in app.json to configure the JavaScript engine. Platform-specific values inside the "ios" or "android" keys will take precedence over the top-level setting. For example, you can set jsEngine to "hermes" at the top level and override it with "jsc" under the "ios" key to use Hermes on Android and JSC on iOS.
Platform API differences between Apple platforms
Platform APIs differ between Apple platforms, most notably in UI frameworks: UIKit is used on iOS and tvOS, while AppKit is used on macOS. Although react-native-macos and expo-modules-core provide aliases and polyfills (e.g., UIView aliased to NSView, UIApplication aliased to NSApplication) for iOS-first libraries, these are usually insufficient for full cross-platform support out of the box.
Expo Modules API platform support
The Expo Modules API provides first-class support for Android and iOS. Support for additional Apple platforms (macOS and tvOS) is available as Out-of-Tree platforms.
Why onStart and onStop are not supported
onStart and onStop Activity lifecycle listeners are not supported because the implementation hooks from ReactActivityDelegate, not MainActivity. ReactActivityDelegate does not have onStart and onStop callbacks.
Interface stability and backwards compatibility
Listener interfaces may change between Expo SDK releases. The backwards compatibility strategy is to add new interfaces and use @Deprecated annotation for interfaces planned for removal. All interfaces use Java 8 interface default methods, so you should only override the methods you need to reduce maintenance costs between SDK updates.
WeakReference prevents memory leaks in lifecycle listeners
When bridging Android lifecycle events to JavaScript, use WeakReference to prevent memory leaks. Create observers with WeakReference to the module instance, and only send events if the WeakReference returns a non-null module.
onBackPressed return value
The onBackPressed callback in ReactActivityLifecycleListener should return a boolean. Return true to prevent the default back button behavior; return false to allow default behavior.
onNewIntent return value
The onNewIntent callback in ReactActivityLifecycleListener should return a boolean. Return true to indicate the intent was handled and prevent default behavior; return false otherwise.
Lifecycle listeners are singleton classes
Lifecycle listeners are singleton classes that exist independently of your Expo module instances. To communicate between a lifecycle listener and your module, you need to observe events from your module and notify the lifecycle listener when events occur.
Deep link handling example with lifecycle listeners
Example showing how to use lifecycle listeners to bridge Android system events to React Native: Create a Package that registers a lifecycle listener, the listener captures deep links from intents in onCreate and onNewIntent, stores initial URL and notifies module observers, the module maintains observers and sends events to JavaScript with bundleOf containing url, scheme, host, and path, then expose via TypeScript module interface and React hook for app usage.
Package interface requirement
Inside your Expo module, create a concrete class that implements the Package interface. For most cases, you only need to implement the createReactActivityLifecycleListeners or createApplicationLifecycleListeners methods.
Application lifecycle callbacks supported
The following Application lifecycle callbacks are currently supported: onCreate, onConfigurationChanged.
Activity lifecycle callbacks supported
The following Activity lifecycle callbacks are currently supported: onCreate, onResume, onPause, onDestroy, onNewIntent, onBackPressed.
Create an ExpoAppDelegateSubscriber
Create a new public Swift class that extends ExpoAppDelegateSubscriber from ExpoModulesCore. Add the class name to the apple.appDelegateSubscribers array in the module config. Run pod install, and the subscriber will be generated in the ExpoModulesProvider.swift file within the application project.
ExpoAppDelegate and AppDelegate subscribers overview
To respond to iOS system events relevant to an app, such as inbound links and notifications, the app AppDelegate must inherit from ExpoAppDelegate. The React Native module API does not provide a mechanism to hook into AppDelegate methods, so the Expo Modules API provides a mechanism for libraries to subscribe to AppDelegate function calls. ExpoAppDelegate implements most functions from UIApplicationDelegate protocol and forwards their calls to all subscribers.
Objective-C not supported for AppDelegate subscribers
Objective-C classes are not supported for AppDelegate subscribers. Only Swift classes that extend ExpoAppDelegateSubscriber can be used.
Unsupported AppDelegate functions
App delegate functions that may cause side effects when provided are not supported yet. For example, application(_:viewControllerWithRestorationIdentifierPath:coder:) is not supported.