Skip to content

Conversation

@renchris
Copy link
Contributor

Summary

Adds a keepPreviousData option (default: true) to useSubscribe that preserves previous subscription data during dependency transitions, eliminating the UI flash that occurs when switching between subscriptions.

Problem

When useSubscribe dependencies change (e.g., navigating /list/AAA/list/BBB), users experience a brief flash where the default value is shown before new data arrives:

T=0ms:   Dependencies change
T=0ms:   Cleanup: setSnapshot(undefined) → component shows default (FLASH!)
T=1-50ms: New subscription fires → component shows new data

History

Version Cleanup Behavior Flash?
v2.11.0 (Mar 2023) setSnapshot(def) ❌ No flash
v4.0.0 (Nov 2023) setSnapshot(undefined) ⚠️ Introduced (masked by batching)
v6.0.0 (Dec 2025) Same ✅ Fully visible after removing unstable_batchedUpdates

The flash was introduced in v4.0.0 (PR #57) as a side effect of type safety improvements, not as an intentional UX decision.

Solution

const todos = useSubscribe(rep, tx => getByCategory(tx, category), {
  default: [],
  dependencies: [category],
  keepPreviousData: true, // default - preserves previous data during transition
});

Behavior:

  • keepPreviousData: true (default): Previous data stays visible until new subscription fires
  • keepPreviousData: false: Immediately shows default value (v4.0.0+ behavior)

Implementation Details

  • prevSnapshotRef: Tracks the most recent successful subscription data
  • Generation counter: Prevents stale subscription callbacks from updating state
  • isMounted guard: Prevents setState after component unmount
  • hasRunEffectRef: Distinguishes initial mount from dependency transitions
  • Error isolation: Try-catch in batched callbacks prevents cascade failures

Breaking Change

This changes the default behavior from v4.0.0+. Users who relied on seeing the default value during transitions can opt-out:

useSubscribe(rep, query, { 
  default: [], 
  keepPreviousData: false // Restore v4.0.0+ behavior
});

Why Default to true

  1. Restores v2.11-v3.x behavior - Smooth transitions were the original design
  2. Matches industry patterns - TanStack Query, SWR recommend keepPreviousData for UX
  3. Flash was unintentional - PR feat: Return default value whenever query function returns undefined. #57 discussion shows no consideration of transition UX
  4. No security benefit to flash - Replicache is user-scoped; server enforces auth on pull

Test Coverage

  • Basic subscribe/unsubscribe behavior
  • keepPreviousData: true preserves data across transitions
  • keepPreviousData: false resets to default
  • Rapid dependency changes (A → B → C)
  • Generation counter ignores stale callbacks
  • Multiple transitions with null/undefined states

Related

Partially addresses #19 (Support suspense in useSubscribe) by eliminating the primary use case for Suspense—avoiding flash during transitions—without requiring Suspense boundaries.


Published as @renchris/replicache-react@6.1.0 for testing.

- Add keepPreviousData option (default: true) to useSubscribe
- Preserve previous snapshot during dependency transitions
- Eliminates UI flash when switching between subscriptions
- Bump version to 6.1.0
Safety improvements:
- Add generation counter to prevent race conditions with stale subscriptions
- Add isMounted guard to prevent setState after unmount
- Detect transitions during render for immediate default when keepPreviousData=false
- Clear prevSnapshotRef in cleanup when keepPreviousData is false
- Capture current snapshot when deps change (shows most recent data, not oldest)

New tests:
- Explicit keepPreviousData: true/false behavior tests
- Rapid dependency changes (A → B → C) race condition test
- Previous data was null test
- Multiple transitions (A → B → A → null → B) test
- Generation counter ignores stale data test

Documentation:
- Comprehensive JSDoc for useSubscribe, Subscribable, UseSubscribeOptions
- Updated README with React 19+ fork section and keepPreviousData feature docs
- Fixed RemoveUndefined type to use Exclude<T, undefined>
- Fix memory leak: always clear prevSnapshotRef on unmount
- Fix unsafe spread: use (dependencies ?? []) for null safety
- Fix edge case: add hasRunEffectRef to properly detect initial mount
  vs transitions (r=undefined at start is now handled correctly)
- Add try-catch in doCallback: isolate errors so one bad callback
  doesn't block others
- Improve types: use ReadonlyArray<unknown> for prevDepsRef
@arv arv requested review from aboodman and arv and removed request for aboodman January 15, 2026 08:40
@arv
Copy link
Contributor

arv commented Jan 15, 2026

Thanks. This makes sense to me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants