Hey, I’m building an app with user-defined saved views over a fairly normalized local data model, and I ran into some performance issues with TanStack DB
Canonical model (simplified)
- threads
- thread_members
- people
- account_people
- person_labels
- labels
Initial approach (fully canonical)
- Fetch filtered + paginated thread IDs from backend
- Store only ordered IDs + cursor locally
- Rebuild visible rows using live queries over canonical collections
- Apply saved-view filters across multiple related collections (incl. nested subqueries)
Architecturally this felt “correct”, but in practice it became very laggy once the dataset reached low-thousands of rows.
The issue wasn’t a single query, but the combination of:
- pagination
- user-defined filters
- multiple collection layers
- nested graph reconstruction from normalized entities
Workaround I implemented:
- Backend returns fully hydrated rows for a saved-view page
- Store them in dedicated collections acting as a read model
- Store query metadata separately:
- ordered row_keys
- cursor
- counts
- UI renders directly from these hydrated collections
This removes the need to rebuild the graph on the fly and gives much better first-render performance
Tradeoff
I now maintain a parallel read model
Canonical optimistic mutations must also patch the hydrated collections
Writes now update both:
- source-of-truth collections
- read-model collections
Questions
Is this "read-model collection” pattern acceptable in TanStack DB?
Or is the intended approach to keep everything canonical and optimize the live-query path?
More broadly: are materialized / read-only collections for large inbox/search views considered a normal pattern, or an anti-pattern?
Would love guidance on how this is supposed to be handled at scale.