Realm Live Results and Change Sets: Stop Re-Fetching After Every Write
Realm's live Results and collection notifications deliver insert, delete and modify indices instead of a generic change signal. Here is the pattern, the thread rules and the lifecycle cost.
02 Apr 2026, 14:22 UTC

The refresh-after-write loop
A familiar shape in mobile data code: the user taps a checkbox, you open a write transaction, set done = true, commit, and then call something like reloadTasks() to re-run the query and hand a fresh array to the list adapter. It works. It also re-reads every row the screen displays and recomputes a full diff in the UI layer, for a change that touched one object.
The alternative Realm is built around: hold the query itself, and let the database tell you which rows moved.
What "live" means for a Results collection
In Realm, a Results collection is not a snapshot of rows. It is a view over the database that reflects committed writes. If you keep a reference to a filtered, sorted query and something else writes a matching object, your reference sees it. You do not re-run the query to get current data.
Two consequences matter in practice:
- Committed writes only. Reads performed inside an open write transaction can differ from what observers receive later, because observers are notified after the transaction commits. Do not treat a read inside the write block as the final state.
- Thread confinement. Realm instances, objects and Results belong to the thread or event loop that opened them. Passing a live object to a background thread is not a supported pattern; use a thread-safe reference or re-query by primary key on the target thread.
What a collection notification actually carries
This is the part that justifies the lifecycle cost. A collection notification is not a bare "something changed" signal. It typically delivers a change set: the indices of inserted, deleted and modified items, and in many SDKs the previous indices of items that moved. That maps directly onto the diffable list APIs modern mobile UI toolkits expect.
Delivery is asynchronous and tied to a run loop or dispatcher. The callback arrives after the write commits, not synchronously inside the write block. Code that assumes the callback has already run by the next line will be wrong.
A worked example: an unfinished task list
Suppose Task has a dueDate and a done flag, and the screen only ever shows unfinished tasks ordered by due date. Model the screen as that query rather than as an array you maintain by hand.
The sketch below is illustrative Swift-shaped pseudocode, not a tested program. API names differ across the Realm SDKs; the shape of the change set is the part that carries over. Confirm exact signatures against your SDK's documentation for your version.
// Runs on the main thread / main dispatcher.
let tasks = realm.objects(Task.self)
.where { $0.done == false }
.sorted(byKeyPath: "dueDate")
// Retain the token for as long as the screen lives.
token = tasks.observe { changes in
switch changes {
case .initial(let results):
applySnapshot(results) // first paint
case .update(let results, let deletions, let insertions, let modifications):
applyChangeSet(deletions: deletions,
insertions: insertions,
modifications: modifications)
case .error(let error):
handle(error)
}
}
Marking a task done is then a short write:
try realm.write {
task.done = true
}
Because the object no longer matches the filter, the notification reports it as a deletion from this collection. The adapter removes one row. Nothing re-reads the table.
Keep write transactions short and focused. A long transaction touching many objects produces one large change set, which is correct but defeats the point of incremental updates.
Trade-offs and failure modes
- Token lifecycle. Notification tokens must be retained while the observer is alive and removed when it goes away. A token that outlives its screen can fire callbacks against destroyed UI.
- Deleted objects throw. Once an object is deleted, accessing its properties raises an error rather than returning stale values. Guard reads, or re-query by primary key after deletions.
- Hidden work. Reactive updates can conceal expensive work that an explicit refresh would have made visible. If a notification handler does heavy computation, that cost is now attached to every write.
- Schema migrations. Changing model classes requires a migration path for existing database files. This is separate from notifications but tends to ship in the same release.
- Support status. The Realm database distributed with MongoDB's Atlas Device SDKs has been subject to deprecation and end-of-support announcements. Confirm the current status for your SDK and release line before starting new work; the behaviour described here is established, not a commitment to future support.
How to check this on your own setup
- Write a small test: open a filtered query, register a notification, insert one matching object and delete another, then assert that the reported insertion and deletion indices match the change you made. This validates the change-set contract for your exact SDK version.
- Test thread confinement deliberately: try to use an object on a background thread and observe the failure mode in your version. Do not rely on it silently working.
- Change a model field and run the app against an existing database file to see what migration behaviour you actually get.
- Check the vendor's current support and deprecation notices before adopting this for a new project.
The decision in one line: if a screen is defined by a query, observe that query and apply the change set; if the screen is assembled from ad-hoc reads across several sources, the notification machinery adds lifecycle cost without removing the refresh.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.