Skip to content

feat(examples): add op-sqlite persistence to React Native offline-transactions demo#1351

Open
kevin-dp wants to merge 9 commits intomainfrom
examples/offline-transactions-react-native-persistence
Open

feat(examples): add op-sqlite persistence to React Native offline-transactions demo#1351
kevin-dp wants to merge 9 commits intomainfrom
examples/offline-transactions-react-native-persistence

Conversation

@kevin-dp
Copy link
Contributor

Summary

  • Adds op-sqlite persistence to the React Native offline-transactions example app, wiring it up with the persistence layer so todos survive app restarts
  • Updates the demo's metro config, database setup, server, and UI components to work with local SQLite storage via op-sqlite

Test plan

  • Run the React Native offline-transactions example on Android/iOS
  • Verify todos persist across app restarts
  • Verify offline mutations are queued and submitted when back online

🤖 Generated with Claude Code

@changeset-bot
Copy link

changeset-bot bot commented Mar 11, 2026

⚠️ No Changeset found

Latest commit: ef13ca7

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 11, 2026

More templates

@tanstack/angular-db

npm i https://pkg.pr.new/TanStack/db/@tanstack/angular-db@1351

@tanstack/db

npm i https://pkg.pr.new/TanStack/db/@tanstack/db@1351

@tanstack/db-browser-wa-sqlite-persisted-collection

npm i https://pkg.pr.new/TanStack/db/@tanstack/db-browser-wa-sqlite-persisted-collection@1351

@tanstack/db-electron-sqlite-persisted-collection

npm i https://pkg.pr.new/TanStack/db/@tanstack/db-electron-sqlite-persisted-collection@1351

@tanstack/db-ivm

npm i https://pkg.pr.new/TanStack/db/@tanstack/db-ivm@1351

@tanstack/db-node-sqlite-persisted-collection

npm i https://pkg.pr.new/TanStack/db/@tanstack/db-node-sqlite-persisted-collection@1351

@tanstack/db-react-native-sqlite-persisted-collection

npm i https://pkg.pr.new/TanStack/db/@tanstack/db-react-native-sqlite-persisted-collection@1351

@tanstack/db-sqlite-persisted-collection-core

npm i https://pkg.pr.new/TanStack/db/@tanstack/db-sqlite-persisted-collection-core@1351

@tanstack/electric-db-collection

npm i https://pkg.pr.new/TanStack/db/@tanstack/electric-db-collection@1351

@tanstack/offline-transactions

npm i https://pkg.pr.new/TanStack/db/@tanstack/offline-transactions@1351

@tanstack/powersync-db-collection

npm i https://pkg.pr.new/TanStack/db/@tanstack/powersync-db-collection@1351

@tanstack/query-db-collection

npm i https://pkg.pr.new/TanStack/db/@tanstack/query-db-collection@1351

@tanstack/react-db

npm i https://pkg.pr.new/TanStack/db/@tanstack/react-db@1351

@tanstack/rxdb-db-collection

npm i https://pkg.pr.new/TanStack/db/@tanstack/rxdb-db-collection@1351

@tanstack/solid-db

npm i https://pkg.pr.new/TanStack/db/@tanstack/solid-db@1351

@tanstack/svelte-db

npm i https://pkg.pr.new/TanStack/db/@tanstack/svelte-db@1351

@tanstack/trailbase-db-collection

npm i https://pkg.pr.new/TanStack/db/@tanstack/trailbase-db-collection@1351

@tanstack/vue-db

npm i https://pkg.pr.new/TanStack/db/@tanstack/vue-db@1351

commit: b7c38db

@github-actions
Copy link
Contributor

github-actions bot commented Mar 11, 2026

Size Change: +1 B (0%)

Total Size: 110 kB

Filename Size Change
./packages/db/dist/esm/index.js 2.86 kB +1 B (+0.04%)
ℹ️ View Unchanged
Filename Size
./packages/db/dist/esm/collection/change-events.js 1.39 kB
./packages/db/dist/esm/collection/changes.js 1.38 kB
./packages/db/dist/esm/collection/cleanup-queue.js 810 B
./packages/db/dist/esm/collection/events.js 434 B
./packages/db/dist/esm/collection/index.js 3.69 kB
./packages/db/dist/esm/collection/indexes.js 2.35 kB
./packages/db/dist/esm/collection/lifecycle.js 1.76 kB
./packages/db/dist/esm/collection/mutations.js 2.47 kB
./packages/db/dist/esm/collection/state.js 5.2 kB
./packages/db/dist/esm/collection/subscription.js 3.71 kB
./packages/db/dist/esm/collection/sync.js 2.43 kB
./packages/db/dist/esm/collection/transaction-metadata.js 144 B
./packages/db/dist/esm/deferred.js 207 B
./packages/db/dist/esm/errors.js 4.83 kB
./packages/db/dist/esm/event-emitter.js 748 B
./packages/db/dist/esm/indexes/auto-index.js 777 B
./packages/db/dist/esm/indexes/base-index.js 766 B
./packages/db/dist/esm/indexes/btree-index.js 2.17 kB
./packages/db/dist/esm/indexes/lazy-index.js 1.24 kB
./packages/db/dist/esm/indexes/reverse-index.js 538 B
./packages/db/dist/esm/local-only.js 890 B
./packages/db/dist/esm/local-storage.js 2.1 kB
./packages/db/dist/esm/optimistic-action.js 359 B
./packages/db/dist/esm/paced-mutations.js 496 B
./packages/db/dist/esm/proxy.js 3.75 kB
./packages/db/dist/esm/query/builder/functions.js 792 B
./packages/db/dist/esm/query/builder/index.js 5.15 kB
./packages/db/dist/esm/query/builder/ref-proxy.js 1.05 kB
./packages/db/dist/esm/query/compiler/evaluators.js 1.62 kB
./packages/db/dist/esm/query/compiler/expressions.js 430 B
./packages/db/dist/esm/query/compiler/group-by.js 2.69 kB
./packages/db/dist/esm/query/compiler/index.js 3.62 kB
./packages/db/dist/esm/query/compiler/joins.js 2.11 kB
./packages/db/dist/esm/query/compiler/order-by.js 1.5 kB
./packages/db/dist/esm/query/compiler/select.js 1.11 kB
./packages/db/dist/esm/query/effect.js 4.78 kB
./packages/db/dist/esm/query/expression-helpers.js 1.43 kB
./packages/db/dist/esm/query/ir.js 784 B
./packages/db/dist/esm/query/live-query-collection.js 360 B
./packages/db/dist/esm/query/live/collection-config-builder.js 7.63 kB
./packages/db/dist/esm/query/live/collection-registry.js 264 B
./packages/db/dist/esm/query/live/collection-subscriber.js 1.94 kB
./packages/db/dist/esm/query/live/internal.js 145 B
./packages/db/dist/esm/query/live/utils.js 1.57 kB
./packages/db/dist/esm/query/optimizer.js 2.62 kB
./packages/db/dist/esm/query/predicate-utils.js 2.97 kB
./packages/db/dist/esm/query/query-once.js 359 B
./packages/db/dist/esm/query/subset-dedupe.js 960 B
./packages/db/dist/esm/scheduler.js 1.3 kB
./packages/db/dist/esm/SortedMap.js 1.3 kB
./packages/db/dist/esm/strategies/debounceStrategy.js 247 B
./packages/db/dist/esm/strategies/queueStrategy.js 428 B
./packages/db/dist/esm/strategies/throttleStrategy.js 246 B
./packages/db/dist/esm/transactions.js 2.9 kB
./packages/db/dist/esm/utils.js 927 B
./packages/db/dist/esm/utils/browser-polyfills.js 304 B
./packages/db/dist/esm/utils/btree.js 5.61 kB
./packages/db/dist/esm/utils/comparison.js 1.05 kB
./packages/db/dist/esm/utils/cursor.js 457 B
./packages/db/dist/esm/utils/index-optimization.js 1.54 kB
./packages/db/dist/esm/utils/type-guards.js 157 B
./packages/db/dist/esm/virtual-props.js 360 B

compressed-size-action::db-package-size

@github-actions
Copy link
Contributor

github-actions bot commented Mar 11, 2026

Size Change: 0 B

Total Size: 4.23 kB

ℹ️ View Unchanged
Filename Size
./packages/react-db/dist/esm/index.js 249 B
./packages/react-db/dist/esm/useLiveInfiniteQuery.js 1.32 kB
./packages/react-db/dist/esm/useLiveQuery.js 1.34 kB
./packages/react-db/dist/esm/useLiveQueryEffect.js 355 B
./packages/react-db/dist/esm/useLiveSuspenseQuery.js 559 B
./packages/react-db/dist/esm/usePacedMutations.js 401 B

compressed-size-action::react-db-package-size

@kevin-dp kevin-dp changed the base branch from cursor/persistence-plan-design-doc-f6d0 to kevin/persistence March 12, 2026 09:22
@kevin-dp kevin-dp force-pushed the examples/offline-transactions-react-native-persistence branch 3 times, most recently from df61339 to 1c7a974 Compare March 12, 2026 10:53
@kevin-dp kevin-dp force-pushed the kevin/persistence branch 4 times, most recently from 37c3bf5 to ca46819 Compare March 16, 2026 11:30
Base automatically changed from kevin/persistence to main March 16, 2026 12:48
@kevin-dp kevin-dp force-pushed the examples/offline-transactions-react-native-persistence branch from ca2c92a to 6798af5 Compare March 16, 2026 13:40
@samwillis
Copy link
Collaborator

Quick review from GPT5.4:


Findings

  1. Medium - examples/react-native/offline-transactions/src/components/TodoList.tsx regresses the initial loading UX. The old version kept a loading state until the query hydrated; this branch now defaults data to [] and immediately shows the empty state whenever the local SQLite cache starts empty. On a cold start with existing server data, users will briefly see “No todos yet” before the first fetch/refetch completes.
const { data: todoList = [] } = useLiveQuery((q) =>
  q.from({ todo: collection }).orderBy(({ todo }) => todo.createdAt, `desc`),
)
{/* Todo list */}
{todoList.length === 0 ? (
  <View style={styles.emptyContainer}>
    <Text style={styles.emptyText}>No todos yet. Add one above!</Text>
    <Text style={styles.emptySubtext}>
      Todos persist in SQLite and sync to server when online
  1. Medium - examples/react-native/offline-transactions/src/db/todos.ts and examples/react-native/offline-transactions/server/index.ts still do not make offline inserts idempotent. syncTodos() receives an idempotencyKey but only logs it; the server accepts client-provided id and blindly overwrites any existing row at that id. If an offline insert is retried after a timeout or reconnect race, the second POST can silently replace the existing record and reset timestamps instead of being safely deduped.
async function syncTodos({
  transaction,
  idempotencyKey,
}: {
  transaction: { mutations: Array<PendingMutation> }
  idempotencyKey: string
}) {
  const mutations = transaction.mutations

  console.log(
    `[Sync] Processing ${mutations.length} mutations`,
    idempotencyKey,
  )

  for (const mutation of mutations) {
    try {
      switch (mutation.type) {
        case `insert`: {
          const todoData = mutation.modified as Todo
          await todoApi.create({
            id: todoData.id,
            text: todoData.text,
            completed: todoData.completed,
// POST create todo
app.post('/api/todos', async (req, res) => {
  console.log('POST /api/todos', req.body)
  await delay(200)

  const { id, text, completed } = req.body
  if (!text || text.trim() === '') {
    return res.status(400).json({ error: 'Todo text is required' })
  }

  const now = new Date().toISOString()
  const todo: Todo = {
    id: id || generateId(),
    text,
    completed: completed ?? false,
    createdAt: now,
    updatedAt: now,
  }
  todosStore.set(todo.id, todo)
  1. Low - examples/react-native/offline-transactions/server/index.ts persists runtime state to server/todos.json inside the example source tree, but there is no ignore rule for that generated file. Running the demo will dirty the repo and future runs will depend on leftover local state unless the file is manually cleaned up.
import { readFileSync, writeFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import cors from 'cors'
import express from 'express'

const app = express()
const PORT = 3001
const DATA_FILE = join(dirname(fileURLToPath(import.meta.url)), 'todos.json')

No additional React Native example findings stood out beyond those on this pass.

@kevin-dp
Copy link
Contributor Author

@samwillis Thanks for the review!

Finding 1 (loading UX) — Good catch, fixed in 8ed3fd3. Restored isLoading from useLiveQuery and gated the empty state so we show a loading indicator while hydrating instead of flashing "No todos yet."

Finding 2 (idempotent inserts) — I think this is fine as-is for a demo. The server's todosStore.set(todo.id, todo) is effectively idempotent since re-inserting the same ID just overwrites with the same data. The only side effect is timestamp reset, which is benign here. Adding idempotency-key tracking to an in-memory Map demo server would add complexity that distracts from the example's purpose (showing the offline-transactions API and the SQLite persistence API surface). The idempotencyKey is provided by the framework for consumers to use in their production backends.

Finding 3 (todos.json) — Good catch, fixed in 970bc7d. Added a .gitignore for server/todos.json.

@kevin-dp kevin-dp force-pushed the examples/offline-transactions-react-native-persistence branch from b29e240 to 4daa9f5 Compare March 18, 2026 12:52
@kevin-dp kevin-dp changed the base branch from main to kevin/persistence-electron March 18, 2026 12:56
@kevin-dp kevin-dp changed the base branch from kevin/persistence-electron to main March 18, 2026 12:57
@kevin-dp kevin-dp changed the base branch from main to examples/offline-transactions-wa-sqlite-demo March 18, 2026 12:57
@kevin-dp kevin-dp changed the base branch from examples/offline-transactions-wa-sqlite-demo to main March 18, 2026 12:57
@kevin-dp kevin-dp force-pushed the examples/offline-transactions-react-native-persistence branch from 4daa9f5 to 005aab7 Compare March 18, 2026 13:04
kevin-dp and others added 8 commits March 18, 2026 14:06
…nsactions demo

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
react-native 0.79.6 accepts react@^19.0.0 as a peer dependency, so
upgrading from the pinned 19.0.0 to ^19.2.4 is safe and fixes the
sheriff version consistency check.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
react-native 0.79.6 bundles a hardcoded react-native-renderer@19.0.0
which must exactly match the react version at runtime. Exclude the RN
example from sherif's version consistency check instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Show a loading indicator instead of the empty state while the query
is still hydrating, preventing a brief "No todos yet" flash on cold
start when existing data needs to be fetched.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The demo server persists todos to server/todos.json at runtime, which
would dirty the working tree without an ignore rule.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@kevin-dp kevin-dp force-pushed the examples/offline-transactions-react-native-persistence branch from 005aab7 to b7c38db Compare March 18, 2026 13:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants