The next generation of Expo APIs: MediaLibrary and Contacts
Product••5 minutes read
Wiktor Smaga
Engineering
SDK 55 brings the next iteration of our core libraries. Shared Objects & Shared Refs fundamentally change how to interact with native data with these libraries.

We believe Shared Objects and Shared Refs are the future for Expo libraries, because they provide deeper integration between modules without unnecessary coupling. To read more about shared objects and shared refs, check out this article.
In SDK 54 we updated the File System with the new object-oriented API. Now, it's time for Contacts and MediaLibrary to get their upgrade. The updated versions of the libraries are available as expo-contacts/next and expo-media-library/next. We will continue to bring these features to other libraries in upcoming SDK releases!
Now, let’s dive into what has changed in the contacts and media library packages.
MediaLibrary@Next
Core concept
The new API is built around a simple idea: Album and Asset are now classes that work as proxies for the native data. They hold a reference to the objects in the system gallery to easily fetch metadata.
To create a new asset, you must pass a local URI to the static Asset.create() function. This command adds a file to the gallery and returns a shared object:
// Create an asset from a local URIconst asset = await Asset.create('file:///path/to/photo.png');
Once you have an asset, managing it is much more intuitive:
// Fetch an album and add the asset directlyconst album = await Album.get('Holiday 2026');await album.add(asset);// Or create a new album and add the asset in one goconst album = await Album.create('Holiday 2026', [asset]);
Direct Access to Properties
The common bottleneck in the old API was the size of the object returned by the getAssetInfoAsync() function. You couldn't simply check the location without fetching a massive plain object containing every single property. MediaLibrary@Next provides granular getters that let you access exactly what you need. No more fetching the entire metadata object just to check the dimensions of an image.
await asset.getLocation(); // { longitude: 50, latitude: 20 }await asset.getShape(); // { width: 100, height: 100 }// Need everything? You can still do that:await asset.getInfo();
Powerful queries
MediaLibrary@Next introduces a new way to filter media. The new Query class draws on the builder pattern, allowing you to create more specific queries in a highly readable way.
It supports the following predicates: gt, gte, lte, lt, eq, within and album - along with orderBy for sorting and limit/offset for pagination.
const assets = await new Query().eq(AssetField.MEDIA_TYPE, MediaType.IMAGE).lte(AssetField.HEIGHT, 1080).within(AssetField.WIDTH, [920, 960, 1080]).orderBy(AssetField.CREATION_TIME).limit(20).offset(10).exe();
Contacts@Next
Core concept
We designed the new API based on the following assumptions:
Contact objects in Contacts@Next hold a reference to the system contacts and contain a lot of async functions to fetch or update the underlying system contacts state. Unlike in the old API, you don't have to pass IDs manually in every function call, and you can rely solely on the update function called on an instance of a specific contact, which avoids a lot of easy-to-make mistakes.
To get started, you can create, fetch, or pick a contact using static methods:
// Create a new contactconst contact = await Contact.create({givenName: 'Andrew',familyName: 'Jones',phones: [{label: 'mobile',number: '+12123456789',},],});// Fetch existing contactconst [contact] = await Contact.getAll({ limit: 1 });// Or pick one using the system UIconst contact = await Contact.presentPicker();
Granular updates
No one likes dealing with massive JavaScript objects just to update a single property. For instance, if you wanted to add a new email address to a contact in the legacy API, you had to modify and send back the entire object:
// THE OLD WAYconst updatedContact = {...contact,emails: [...(contact.emails || []), { address: 'contacts-next@expo.dev', label: 'work' }],};await Contacts.updateContactAsync(updatedContact);
Now you can perform the same operation with just one line of code:
// THE NEW WAYawait contact.addEmail({ address: 'contacts-next@expo.dev', label: 'work' });
And you can do this with every contact property! Look how easy it is to set a profile picture now:
const result = await ImagePicker.launchImageLibraryAsync();await contact.setImage(result.assets[0].uri);
Performant querying
The same goes for querying - if you want to fetch just one specific field, you can use granular getters:
const givenName = await contact.getGivenName();
To get more than just one property, use getDetails. Simply pass an array of the fields you are interested in:
await contact.getDetails([ContactField.EMAILS, ContactField.FULL_NAME]);
And if you are building a list of contacts, there is a more efficient function for this: getAllDetails(). Under the hood, it uses a single system API call, making it very performant. It also supports different types of filtering as an optional parameter.
// This is a static module function.await Contact.getAllDetails([ContactField.EMAILS, ContactField.FULL_NAME]);
Additionally, objects returned by these functions have a narrowed type based on the fields requested in the arguments. TypeScript will warn you if you try to access a field that hasn't been fetched.
const details = await contact.getDetails([ContactField.GIVEN_NAME]);console.log(details.givenName); // ✅ "John"console.log(details.phones); // ❌ TypeScript will show a warning
.patch() + .getDetails() = <3
The new version of the library includes a patch() method - similar to the one in the HTTP protocol - which applies partial modifications. It skips undefined properties, so you only update the fields you actually want to change.
It has been designed to work smoothly with getDetails(): while getDetails() fetches only requested fields, patch() updates only the defined ones. This is perfect for forms where you only change one or two fields:
const details = await contact.getDetails([ContactField.EMAILS, ContactField.GIVEN_NAME]);details.givenName = 'John';await contact.patch(details); // Only updates the givenName in the system.
If you want to completely replace the contact properties, you can still use the update method. Unlike patch, it overrides the entire contact:
await contact.update({givenName: 'New Name',familyName: 'New Surname',// Other fields will be cleared});
Check out the new documentation
Since this is the next iteration of these libraries, your input is more important than ever. We’re open and extremely grateful for any feedback, GitHub issues, feature requests, or tweets you want to send our way.
We also invite you to check out the new docs, where you will find examples of how the APIs work in action!



