← Application platforms · Chapter 5

Design offline notes with React Native and Expo

Design React Native and Expo reading notes with durable local records, a synchronization queue, idempotent requests, explicit conflicts, and platform-specific permissions.

After this guide, you can

Implement notes and pending operations, then verify offline restart, duplicate retries, conflict handling, and separate storage for credentials and content.

Reading notes can be saved on a device before reaching a server. Editing offline and resuming later without applying one edit twice requires durable storage, operation identity, and server acknowledgement. React screen state handles only part of this workflow.

Use synthetic note N-1 and operation OP-1. The first version includes a list, editing, and explicit synchronization. It requests no camera or contacts access and promises no uninterrupted background execution. Attachment permissions can be added when attachments exist.

Understand the platform boundary

React Native describes interfaces through React components backed by platform capabilities. View and Text are not browser DOM elements. Browser styles and file interfaces cannot be copied unchanged. Expo supplies modules, tooling, and project structure, while native configuration and platform artifacts still need validation.

Expo Go and development builds support different sets of native modules. Check the selected SDK documentation before adding storage or file capabilities. A successful JavaScript import does not prove the installed application contains the required native implementation.

Use the current official creation instructions and preserve their version conditions. At source-check time, the page describes an SDK 58 transition in which explicit templates and default creation can select different versions. Choose a template compatible with the intended environment and record the generated SDK instead of treating a course version as permanent.

Design durable records first

A note contains ID, body, local edit sequence, server baseline, and modification time. A separate operation contains ID, note ID, submitted content, baseline version, and status. Keep a submission snapshot, not a reference to changing screen values. A retry must send the same operation content.

Saving the note and enqueueing its operation should share a transaction boundary. Confirm local saving only after the write succeeds and pending status only when work is stored. Reload the list from the repository; useState cannot provide recovery after process termination.

Choose storage by data requirements. Expo documents SQLite as persistent database storage and Async Storage as unencrypted asynchronous key-value storage. The latter suits small nonsensitive preferences, not credentials. Evaluate SecureStore for small secrets while managing document data separately.

Connect the engineering components

Within the selected Expo version, create note types, repository, list, and editor. Use TextInput for the body and display separate save and synchronization states. Begin with an in-memory repository, then install persistent modules using their official instructions and retain native configuration requirements.

Create versioned database tables. On startup, open storage and complete migrations before querying. Report read errors rather than clearing the database. Save at a defined frequency and handle pending edits during rapid note changes. Navigation warnings should reflect content that is actually unsaved.

Next create a controlled server endpoint validating account identity, ownership, field size, and baseline version. Return the operation ID and acknowledged version. The client clears only the acknowledged operation, not the whole queue. Implement active synchronization before evaluating allowed background mechanisms.

Retry with stable operation identity

A timeout can happen after the server has saved data. Retry using the same operation ID, allowing the server to recognize it and return the earlier result. Disabling repeated taps cannot address lost responses or restarts.

The Python model tracks processed operation IDs in a set. It checks that OP-1 updates a note once, without transactions, networking, authentication, or Expo execution.

server = {}
seen = set()
def sync(operation):
    if operation["id"] in seen:
        return False
    server[operation["note"]] = operation["text"]
    seen.add(operation["id"])
    return True
operation = {"id": "OP-1", "note": "N-1", "text": "draft"}
assert sync(operation)
assert not sync(operation)
assert server["N-1"] == "draft"
print(server)

The output is {'N-1': 'draft'}. The first attempt returns true and the second false. A real server needs an appropriate transaction covering its idempotency record and business update. This in-memory set disappears with the process and cannot provide durable deduplication.

Resolve conflicts explicitly

An outdated baseline should produce a conflict, including the current version or a way to fetch it. Preserve the local draft and offer explicit comparison and resolution. Retrying a temporary failure does not make an outdated baseline valid.

A merged edit becomes a new operation using the latest acknowledged baseline. Mark the old operation resolved or superseded instead of retrying indefinitely. If a response arrives after further editing, acknowledge its own snapshot without marking later edits synchronized.

Phone timestamps alone are unreliable overwrite rules because clocks differ and concurrent content may both matter. Represent deletion as an identifiable synchronization operation so an offline client cannot casually resurrect removed notes.

Recover errors and permissions

Request system access only for an implemented feature and provide behavior when permission is denied. Plain note entry needs no contacts access. File selection should be user-triggered and explain uploaded content and purpose.

On startup, enumerate pending operations and make interrupted sends retryable. Renew expired authentication without logging tokens. Insufficient storage must leave an unsaved indicator. Check target-platform uninstall, backup, and storage behavior; local persistence is not a guarantee against every kind of data loss.

Test composition input and rapid note changes. Do not reset the cursor after every input event. Associate saves with a note ID and content snapshot; late acknowledgement after switching updates only that note. Distinguish persisted content from unsaved editor input.

Bound the synchronization queue instead of sending unlimited parallel work. Respect per-note version dependencies, delay temporary failures, and retain validation errors for correction. Any coalescing must preserve the identity of sent operations and the meaning of deletion. Verify dependent operations before adding batching.

Exercise: verify divergence and recovery

Create a note, edit offline, terminate, and reopen. Both body and queued work must survive. Simulate a processed request with a lost response; resending its ID must cause only one effective update.

Then simulate another client advancing the server version. Preserve the local draft in conflict state and create a new merged operation. Check keyboard behavior, restart recovery, and module configuration separately on Android and iOS. Compare screen, database, and acknowledgements. Python deduplication alone does not establish a complete synchronization application.

Sources & further reading

Sources checked on 2026-11-10. Original engineering explanation; examples are teaching data, not production measurements.