Skip to content

Migrate Transportation view to TanStack DB#7583

Draft
hawkrives wants to merge 20 commits into
masterfrom
claude/migrate-transportation-tanstack-WicOa
Draft

Migrate Transportation view to TanStack DB#7583
hawkrives wants to merge 20 commits into
masterfrom
claude/migrate-transportation-tanstack-WicOa

Conversation

@hawkrives

@hawkrives hawkrives commented Apr 28, 2026

Copy link
Copy Markdown
Member

Summary

Migrates the Transportation view from TanStack Query + useState/useEffect to TanStack DB 0.6 collections backed by SQLite persistence.

Goals:

  • A — Offline persistence: Bus routes and other-modes data survive app restarts via SQLite (@op-engineering/op-sqlite)
  • B — Reactive derived state: Clock-driven useState/useEffect chains in BusLine replaced by inline derivation — timer tick is an input to render, not a trigger for effect chains
  • D — Normalized data model: Bus lines and other-modes stored as keyed collections; direct lookup replaces linear array search

Approach:

  • @tanstack/db queryCollectionOptions uses the existing QueryClient internally, so TanStack Query stays in the project for the fetch lifecycle (stale time, refetch interval, online/focus management)
  • SQLite initialized once at app startup via source/init/tanstack-db.ts
  • busLinesCollection and otherModesCollection replace the existing query.ts files
  • Sync strategy: re-fetch on access if data > 1 hour old (staleTime), pull-to-refresh via collection.utils.refetch(), background hourly refresh only while view is mounted (refetchInterval)

New packages: @tanstack/db@0.6.5, @tanstack/react-native-db-sqlite-persistence@0.1.9, @op-engineering/op-sqlite@15.2.12

API Discovery Notes

@tanstack/db 0.6.5 does not export React hooks (useQuery, queryCollection). React integration uses useSyncExternalStore with:

  • collection.subscribeChanges() — fires when collection data changes
  • collection.on('status:change', ...) — fires on collection lifecycle transitions (idle→loading→ready)
  • queryClient.getQueryCache().subscribe() — catches background refetch errors that leave the collection in ready state (so status:change doesn't fire)

queryCollectionOptions is in a separate package: @tanstack/query-db-collection.

Status

All tasks complete ✅

  • Task 1: Install packages and configure Jest
  • Task 2: Create SQLite init module and wire app startup
  • Task 3: Create busLinesCollection
  • Task 4: Create otherModesCollection
  • Task 5: Simplify bus/wrapper.tsx
  • Task 6: Simplify bus/line.tsx (remove useState/useEffect derivation)
  • Task 7: Update other-modes/list.tsx
  • Task 8: Delete old query files
  • Task 9: Final verification

Known follow-up

  • getUtilsSnapshot() creates a new object on every call, so every query cache tick triggers a re-render of the collection views even if isLoading/isError values didn't change. Fix: field-level comparison before updating utilsRef.current. Applies to both bus/wrapper.tsx and other-modes/list.tsx.
  • Pre-existing bug in bus/line.tsx: midnight-advance useEffect compares newCurrentDay (derived from same now) against currentDay (also derived from same now) — they're always equal, so auto-advance never fires. Out of scope for this migration.

Design

Full design spec: docs/superpowers/specs/2026-04-28-transportation-tanstack-db-design.md
Implementation plan: docs/superpowers/plans/2026-04-28-transportation-tanstack-db.md


Generated by Claude Code

claude and others added 20 commits April 28, 2026 02:50
Installs @tanstack/query-db-collection and implements busLinesCollection
using queryCollectionOptions (TanStack Query sync) wrapped in
persistedCollectionOptions (SQLite persistence) when available, with
graceful fallback when SQLite init has failed.

https://claude.ai/code/session_01PFCyQJtzT7odhBmaoDMMLr
…ions

Restores AbortSignal passthrough from QueryFunctionContext so TanStack
Query can cancel in-flight requests, and replaces the unsafe `as` cast
with typed `.json<{data: T[]}>()` plus a `select` option to extract the
items array from the wrapped API response.

https://claude.ai/code/session_01PFCyQJtzT7odhBmaoDMMLr
Replaces the TanStack React Query useQuery(busRoutesOptions) + Array.find
pattern with a direct busLinesCollection subscription via useSyncExternalStore,
using the collection's toArray getter and utils.{isLoading,isError,lastError,refetch}.

https://claude.ai/code/session_01PFCyQJtzT7odhBmaoDMMLr
Subscribe to both subscribeChanges (for data) and status:change (for
loading/error transitions) so the component re-renders when fetch status
changes. Use useRef to cache stable array and utils snapshots, fixing the
infinite re-render loop caused by toArray returning a new reference on
every call.

https://claude.ai/code/session_01PFCyQJtzT7odhBmaoDMMLr
…tion views

status:change only fires on collection lifecycle transitions (idle→loading→ready).
Background refetch errors leave the collection in 'ready' state, so isError/isLoading
changes from QueryObserver never trigger a re-render.

Fix: subscribe to queryClient.getQueryCache() in both BusView and OtherModesView so
all QueryObserver-level state transitions (background refetch loading, post-initial
errors) update utilsRef and trigger a re-render. Also update utilsRef inside
subscribeChanges so data-arrival always syncs utils state.

https://claude.ai/code/session_01PFCyQJtzT7odhBmaoDMMLr
bus/query.ts and other-modes/query.ts are fully replaced by collection.ts
files. No remaining imports reference them.

https://claude.ai/code/session_01PFCyQJtzT7odhBmaoDMMLr
…t JSX props

- Remove unnecessary `async` keyword from queryFn in both collections (no
  await expression; .json() already returns a Promise — fixes require-await lint)
- Use `isRefetching` instead of `isLoading` for SectionList's `refreshing` prop
  so the pull-to-refresh spinner shows during background refetches, not just
  initial load
- Sort SectionList JSX props alphabetically to satisfy react/jsx-sort-props rule
- Prettier formatting for tanstack-db.ts and wrapper.tsx

https://claude.ai/code/session_01PFCyQJtzT7odhBmaoDMMLr
getQueryCache().subscribe() fires for every query in the app. Without
filtering, BusView and OtherModesView would re-render on every unrelated
network response (dining menus, calendar, directory, etc.).

Filter to the collection's own query key so only transitions in the
transit/bus-routes and transit/modes queries trigger a utils update.

Also document the module initialization ordering requirement in tanstack-db.ts:
this file must run before any collection module reads the `persistence` export.

https://claude.ai/code/session_01PFCyQJtzT7odhBmaoDMMLr
refetch: (typeof busLinesCollection.utils)['refetch']
}

function getUtilsSnapshot(): UtilsSnapshot {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to play with this locally

Comment on lines +29 to +34
? persistedCollectionOptions<
UnprocessedBusLine,
string,
never,
QueryCollectionUtils<UnprocessedBusLine, string>
>({

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to play with this locally and see if I can get it to infer the types

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.

3 participants