Articles · How toUpdated October 2026

A bookmark is one row in a table, and where you keep that table decides if it comes back.

To add bookmarks or favourites, save one row per item and read the rows back as a list. In an Expo project the store that the documentation actually makes a promise about is expo-sqlite, whose reference says the database is persisted across restarts of your app. That covers a reboot and a force quit. It does not cover a reinstall, and neither does anything else: no Expo storage reference promises your data back after someone deletes the app. If you are earlier than this and still choosing how to build the thing at all, start from make an app.

What follows is the store Expo documents for a list that has to survive a restart, what the documentation says and does not say about a reinstall, what Apple publishes about carrying a small list across somebody's devices with iCloud, and the four things the heart button itself has to get right. Every quotation here was read from the vendor documentation on 23 October 2026.

Start here

The short version

The feature is easy. The promise you make about it is the hard part.

Nobody files a bug that says the insert failed. They file one that says their saved recipes are gone. That happens because the list was kept on the device and the device changed, and because the app never said which of those two things it was offering. So decide first, in one sentence, what you are promising. The list lasts until you delete the app. Or the list follows your account. Those are different products and they cost different amounts.

Three homes exist for the list. A table on the device, which Expo documents as lasting through restarts. The iCloud key-value store, which Apple documents as syncing a small amount of data between one person's devices, with a published quota and no Android. Or your own server behind an account, which is the only one that gets a list back onto a phone that was wiped. Pick by what you promised, not by what is quickest.

Where does a bookmark list actually live?

In a SQLite database on the phone, if it only has to be there next time the app opens. The Expo SQLite reference opens with the sentence that matters: the database is persisted across restarts of your app. That is the strongest promise in Expo's storage documentation, and it is enough for a reboot or a force quit. A phone that died in somebody's pocket counts as a restart too.

The shape is small. You wrap the app in SQLiteProvider with a databaseName, reach the handle anywhere with the useSQLiteContext hook, and run the schema once in the onInit callback. Expo's own migration example reads PRAGMA user_version, compares it to a version constant in your code, creates the table when the two do not match and writes the new version back. For a favourites table that is one CREATE TABLE with the item id and a saved time. Saving is a runAsync insert. Unsaving is a runAsync delete. The list screen is a getAllAsync select, and the filled or empty heart on an item screen is a getFirstAsync select for one id.

Two details in that reference save time later. There is a key-value layer in the same package, exposed as expo-sqlite/kv-store, which Expo documents as a drop-in replacement for the Async Storage library and which adds synchronous calls like setItemSync and getItemSync. If you already depend on expo-sqlite you do not need a second storage dependency for the handful of small settings around the feature. And Expo notes that on Apple TV the database file sits in the caches directory rather than the application documents directory, which means a tvOS build has a weaker guarantee than the phone build from the same code.

Expo, SQLite reference, read 23 October 2026

Check yours

Where your favourites list belongs

Tick every line that has to be true of your saved list. Each answer names the store and what the documentation actually promises about it, read on 23 October 2026.

Nothing ticked. A favourites list that only has to survive a reboot is a table with two columns and an afternoon of work. Everything that makes it harder is on the list above.

Will the list survive a reinstall?

Assume no. This is the question that gets answered wrongly most often, so here is what the documentation says rather than what people repeat. Expo discusses what happens when an app is uninstalled in exactly one reference, SecureStore, under a heading called Data persistence. On Android, it says, data saved using expo-secure-store will not be preserved upon app uninstallation. On iOS it says the data will persist across app uninstallations if the app is reinstalled with the same bundle ID, because of the way the iOS Keychain manages stored credentials, and then adds that this is not guaranteed and you should never rely on this implementation detail. A vendor telling you not to rely on its own behaviour is as clear as these pages get.

The other storage references do not make the claim at all. The SQLite reference stops at restarts. The FileSystem reference describes the document directory as a place to store files that are safe from being deleted by the system, and the cache directory as a place for files the system can delete when the device runs low on storage, and says nothing about an uninstall either way. The Expo page for Async Storage is a stub: it calls the library persistent in one line and links out to the library's own site. Searching the rendered pages for the word uninstall returns nothing on any of the three, and on the Async Storage page even the word restart is absent. Checked on 23 October 2026.

So if your answer to a customer has to be that their saved list comes back, the list has to be attached to an account on a server you run, and the device copy becomes a cache of it. That turns bookmarks into the same kind of feature as a shared timeline, which is why add activity feed runs into the same account question from a different direction. The good news is that one server-side list costs about as much to build as two, so people who need both should plan them together.

Expo, SQLite reference, the restart wording quoted above, read 23 October 2026

Can the list follow someone to their other devices?

On Apple platforms, yes, and Apple documents the mechanism for exactly this size of data. NSUbiquitousKeyValueStore is described as an iCloud-based container of key-value pairs you share among instances of your app running on a person's devices. It has been there since iOS 5. The example Apple puts in the overview is a textbook app saving the current page number on an iPhone so the person can carry on from the same place on another device, which is the same shape as a list of saved item ids. Values have to be property list types, so an array of strings goes in without any encoding. When a value changes on one device, iCloud forwards the change and the system posts didChangeExternallyNotification on the others, and you register for that to keep every copy current.

Read the quota before you commit to it. Apple states that an app can have no more than 1024 keys in the store, that the total space for all values is 1 megabyte, that the largest single value is also 1 megabyte, and that a key string can be at most 128 characters in UTF-16 with key strings excluded from the value quota. A write that would break a limit fails without adding anything, and the system posts the change notification with its reason set to NSUbiquitousKeyValueStoreQuotaViolationChange. One array of ids fits comfortably. A saved copy of each article does not. Figures read on 23 October 2026.

Then read the conditions. Apple says not to store personal or sensitive information in the key-value store because the system keeps it on disk in an unencrypted format, and points you to the Keychain instead, so think about whether a list of what somebody has saved counts. Apple also notes that you must distribute the app through the App Store or Mac App Store and request the iCloud key-value store entitlement in Xcode, which pulls the store submission into the plan rather than leaving it to the end: publish to app store covers that side. And this is an Apple service, so Android users get nothing from it. Expo ships no module for the key-value store either: the consolidated documentation index at docs.expo.dev slash llms.txt lists every page, and it has no entry matching iCloud, ubiquitous or key-value, so reaching this API from an Expo project is native code behind a config plugin.

Apple, Foundation documentation, NSUbiquitousKeyValueStore, read 23 October 2026

Three homes for a favourites list, side by side

Each row is a job somebody will expect the feature to do. The first two columns are what the vendor documentation states. The third is what you take on yourself. Read down your own column and you will find the row you were planning to ignore.

The jobexpo-sqlite on the deviceApple iCloud key-value storeYour server behind an account
Survives the phone restartingYesYesYes
Survives the app being deleted and installed againNoYesYes
Shows up on the same person's other devicesNoYesYes
Saving works with no networkYesThe write is local first, then forwarded when iCloud decidesNo
Documented size ceilingNone stated in the reference1024 keys, 1 megabyte of values in totalWhatever you pay for
Needs an entitlement and App Store distributionNoYesNo
Works on AndroidYesNoYes
Someone has to run and pay for itNoNoYes

What does the heart button itself have to get right?

Write locally and fill the heart immediately. The tap should never wait for a network call, because the person taps while scrolling and looks away. Apple makes the same point from the other side in the documentation for the synchronize method: call it sparingly, typically only at launch or when the app returns to the foreground, and do not rely on values being available on the other devices straight away. Apple adds that the method does not force a write, it tells iCloud that new values exist, and that iCloud typically limits updates to several times per minute. A heart tapped eight times in ten seconds is a local write with a sync that catches up afterwards. There is no design where it is eight network calls.

Put a unique index on the id column so a double tap cannot create two rows, and let the database refuse the duplicate instead of guarding it in the handler. Read the saved state from the table rather than from component state, because the two drift apart as soon as a sync writes a row the screen has not seen. And decide what a removal is. If the only trace of an unsave is a missing row, a phone that was offline at the time will push its old copy back and the bookmark returns from the dead. A removal marker with a timestamp, with the newer timestamp winning, is the usual fix, and it is only worth the work if the list syncs at all.

The last one is guests. People save things before they sign in, which is good, and it leaves you with a device list and an account list that both exist at sign-up. Merge them, dedupe by item id, and keep the earlier saved time. Rewarding that sign-up is a related decision, and add referral program depends on the same stable account id that the merge produces.

Newly builds this kind of screen from a plain English description. It writes a real React Native and Expo project you own, with two-way GitHub sync, so the table and the tap handler are ordinary code you can read and change. It uploads iOS builds to TestFlight, publishes to Google Play internal testing and builds a standalone Android APK. It costs $25 a month and there is no free tier. It will not press submit on the App Store for you, because Apple guideline 4.2.6 requires the provider of the content to submit, so that step stays yours.

Apple, Foundation documentation, NSUbiquitousKeyValueStore and its synchronize method, read 23 October 2026

Questions people ask about bookmarks and favourites

Store one row per saved item and read it back as a list. In an Expo project the documented store for that is expo-sqlite, whose reference opens by saying the database is persisted across restarts of your app. You create a table with the item id and the time it was saved, insert on tap with runAsync, delete on a second tap, and read the whole list with getAllAsync. The screen that shows a filled or empty heart asks the same table whether a row exists. That is the entire feature on one device, and it is a small amount of code. Everything that makes bookmarks hard is about what happens to that table later.

Write the promise down first.

One sentence about what happens to a saved list when the phone changes. That sentence picks the store and tells you whether the heart button is an afternoon of work or a fortnight. Then describe the screens.

Start building