Skip to content

fix(query): refetch on remount after mutation while unmounted [#2986]#2988

Merged
viniciusdacal merged 1 commit intomainfrom
fix/query-revalidation-on-navigate
Apr 23, 2026
Merged

fix(query): refetch on remount after mutation while unmounted [#2986]#2988
viniciusdacal merged 1 commit intomainfrom
fix/query-revalidation-on-navigate

Conversation

@viniciusdacal
Copy link
Copy Markdown
Contributor

Closes #2986.

Summary

  • MutationEventBus now tracks a monotonic per-entity-type version that increments on every emit(); getVersion(entityType) exposes it.
  • MemoryCache.set(key, value, version?) records the stamp; CacheStore<T> gained an optional getVersion(key).
  • query() stamps every cache write with the bus version for its entity type and skips cache hits whose stamp is older than the current bus version. SSR-hydrated entries are explicitly stamped 0 so a client emit that lands before hydration completes correctly marks the SSR payload stale on remount. Nav-prefetch cache-hit paths (init-time and effect-time) honor the same check, using the descriptor's _entity when the query's entityMeta hasn't been assigned yet.

Why

Navigating list → form → back via router.navigate() left the list showing pre-mutation data until a full page reload. The list query had unsubscribed from the bus on navigate-away, so the emit() fired by the form's mutation reached no live listener — yet the cached entry and its query indices remained and were served on remount. emit() only notified live subscribers; it didn't touch the cache.

Public API Changes

  • MutationEventBus.getVersion(entityType: string): numbernew.
  • CacheStore<T>.set(key, value, version?)added optional version parameter (backward compatible).
  • CacheStore<T>.getVersion(key): number | undefinednew optional method. Custom cache implementations without it keep the previous behavior.

No breaking changes. Entity-backed queries automatically participate. Non-entity queries are unaffected.

Test plan

  • vtz test — 2481 tests pass in @vertz/ui
  • vtz test — 1285 tests pass in @vertz/ui-server
  • vtz run typecheck — clean
  • vtzx oxlint — no new warnings (16 pre-existing, 16 after)
  • vtzx oxfmt --check — clean
  • New regression tests in packages/ui/src/query/__tests__/query.test.ts:
    • Direct descriptor: mount → dispose → emit same type → remount → fresh fetch
    • Descriptor-in-thunk: same flow, with lazy entity metadata
    • Custom key: same flow
    • Cross-entity isolation: emit on unrelated type → cache still served
  • New unit tests in packages/ui/src/query/__tests__/cache.test.ts (version stamp: set/get/delete/clear/LRU/v0)
  • New unit tests in packages/ui/src/store/__tests__/mutation-event-bus.test.ts (getVersion: initial state, increments per type, reset on clear)

Pre-existing out-of-scope note

The effect calls raw._fetch() eagerly on every run before the cache-hit check is evaluated. On a cache hit the resulting promise is suppressed via .catch, but the fetch call itself has already started. Worth revisiting separately — unrelated to this fix.

Closes #2986.

When a user navigated list → form → back via `router.navigate()` after
creating an entity, the list kept showing the pre-mutation data until a
full reload. The list query had unsubscribed from `MutationEventBus` on
navigate-away, so the `emit()` fired by the form's mutation reached no
live listener — yet the cached entry and its query indices remained and
were served on remount.

Fix:
- `MutationEventBus` tracks a monotonic per-entity-type version that
  increments on every `emit()`; `getVersion(entityType)` exposes it.
- `MemoryCache.set(key, value, version?)` records the stamp; `CacheStore<T>`
  gained an optional `getVersion(key)` accessor.
- `query()` stamps each cache write with the current bus version for its
  entity type and skips cache hits whose stamp is older than the bus
  version — falling through to a fresh fetch. SSR-hydrated entries are
  stamped `0` so a client emit that lands before hydration completes
  correctly marks the SSR payload stale on remount.
- Nav-prefetch cache-hit paths (init-time and effect-time) also honor
  the staleness check, using the descriptor's `_entity` when the query's
  `entityMeta` hasn't been assigned yet.

Unstamped entries are treated as version 0 so custom `CacheStore`
implementations without `getVersion` regress to the previous behavior
rather than breaking the build.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@viniciusdacal viniciusdacal merged commit 5b3838b into main Apr 23, 2026
7 checks passed
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.

query: stale list served on remount after mutation while unmounted

1 participant