Normalized entity caching — interest check and design discussion #531
Replies: 2 comments
-
|
Beta Was this translation helpful? Give feedback.
-
|
Hey @posva, thank you for the response! I'm happy to create and maintain this package--I whipped up a working implementation. TL;DR: The plugin is published as The app.use(PiniaColada, {
plugins: [
PiniaColadaNormalizer({
entities: {
contact: defineEntity({ idField: 'contactId' }),
},
}),
],
})
// Opt in per query:
const { data } = useQuery({
key: ['contacts'],
query: () => fetchContacts(),
normalize: true,
})
// WebSocket writes propagate to all queries automatically:
const entityStore = useEntityStore()
ws.on('CONTACT_UPDATED', (payload) => {
entityStore.set('contact', payload.contactId, payload)
})Re: #1 Re: #2 (read interception) Re: #3 (schema config)
~10KB gzip, zero runtime dependencies. Would love your feedback on the API surface. Created with assistance from Claude Code, vetted by HITL (DannyDevs) |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
The problem
Pinia Colada's cache is document-based: each query key maps to one data blob. When the same entity appears in multiple queries, it lives as independent copies that can diverge:
For most apps, this is fine — you invalidate and refetch. But in WebSocket-heavy apps where the server pushes entity updates, you want a single write to propagate everywhere instantly — no invalidation, no refetch. That requires normalized storage: entities stored once by type+ID, queries holding references instead of copies.
This is the same problem Apollo Client solved for GraphQL. I haven't found an equivalent for REST/generic data-fetching in the Vue ecosystem — if I'm wrong, I'd love to know about it.
What I'm proposing
A normalization plugin that:
setEntryStatevia$onAction, normalizes incoming data using a schema (normalizr-inspired), and stores entities in a shared Pinia store. The query entry keeps normalized references instead of full objects.The write path is straightforward —
setEntryStateis the canonical choke point, andsetQueryDataflows through it. Theextendandremovehooks handle lifecycle.The read path is the interesting design question, and that's mainly what I'd like your input on.
Background
I've built this pattern in production — replacing TanStack Query with Pinia + normalizr in a WebSocket-heavy Vue 3 app. Entities stored once, WS events writing directly to normalized stores, all views updating reactively. It works well, and I'd like to generalize the approach into something the broader Pinia Colada ecosystem could use.
Questions
Is this something you'd want as an official plugin, or better as a community package? Happy either way — just want to align before writing code.
The read interception problem:
useQueryreadsentry.state.value.datathrough a computed with no plugin hook in that path. I noticed the delay plugin replacesentry.asyncStatusduringextendanduseQueryreads through the replacement seamlessly — would a similar pattern forentry.statebe reasonable here? Or would you prefer something different, like a lightweighttransformDatahook in the data path? (Your comment on Discussion #113 aboutselectbeing a plugin concern feels adjacent.)Schema-to-query mapping: The plugin needs to know which schemas apply to which query keys. I'm leaning toward a top-level config (
{ schemas: { contacts: contactSchema } }) rather than per-query options. Does that align with how you think about plugin configuration?Happy to build a proof-of-concept first if that's more useful than discussing in the abstract.
Beta Was this translation helpful? Give feedback.
All reactions