TODO: review
This example demonstrates how to properly coordinate Browser URL update with React transitions and implement instant back/forward navigation via caching in a React Server Components application.
In a typical RSC application with client-side navigation, there's a challenge in coordinating:
- Browser history changes (pushState/replaceState/popstate)
- React transitions for smooth updates
- Asynchronous data fetching
- Loading state indicators
- Back/forward navigation performance
Without proper coordination, you can encounter:
- URL bar being out of sync with rendered content
- Slow back/forward navigation (refetching from server)
- Issues with cache invalidation after mutations
- Missing or inconsistent loading indicators
This example implements a caching pattern that addresses these issues:
- Modern Navigation API: Uses Navigation API when available, falls back to History API
- Back/Forward Cache by Entry: Each navigation entry gets a unique key, cache maps
key → Promise<RscPayload> - Instant Navigation: Cache hits render synchronously (no loading state), cache misses show transitions
- Dispatch Pattern: Uses a dispatch function that coordinates navigation actions with React transitions
- Promise-based State: Navigation state includes a
payloadPromisethat's unwrapped withReact.use() - Cache Invalidation: Server actions update cache for current entry
The implementation automatically detects and uses:
- Navigation API (Chrome 102+, Edge 102+): Modern, cleaner API with built-in entry keys
- History API (all browsers): Fallback for older browsers, requires manual key management
No configuration needed - feature detection happens automatically!
The core implementation is in src/framework/navigation.ts:
// Feature detection
const supportsNavigationAPI = 'navigation' in window
// Navigation API: Clean, modern
private listenNavigationAPI(): () => void {
const onNavigate = (e: NavigateEvent) => {
if (!e.canIntercept) return
e.intercept({
handler: async () => {
this.navigate(url.href)
},
})
}
window.navigation.addEventListener('navigate', onNavigate)
return () => window.navigation.removeEventListener('navigate', onNavigate)
}
// History API fallback: Works everywhere
private listenHistoryAPI(): () => void {
window.history.pushState = (...args) => {
args[0] = this.addStateKey(args[0])
this.oldPushState.apply(window.history, args)
this.navigate(url.href)
}
// ... popstate, replaceState, link clicks
}
// Dispatch coordinates navigation with transitions and cache
dispatch = (action: NavigationAction) => {
startTransition(() => {
setState_({
url: action.url,
push: action.push,
payloadPromise: action.payload
? Promise.resolve(action.payload)
: bfCache.run(() => createFromFetch<RscPayload>(fetch(action.url))),
})
})
}
// Each history entry gets a unique key
function addStateKey(state: any): HistoryState {
const key = Math.random().toString(36).slice(2)
return { ...state, key }
}Why this works:
React.use()can unwrap both promises AND resolved values- Cache hit → returns existing promise →
React.use()unwraps synchronously → instant render, no transition! - Cache miss → creates new fetch promise →
React.use()suspends → shows loading, transition active - Browser automatically handles scroll restoration via proper history state
pnpm install
pnpm devThen navigate to http://localhost:5173
-
Cache Behavior:
- Visit "Slow Page" (notice the loading indicator)
- Navigate to another page
- Click browser back button
- Notice: No loading indicator! Instant render from cache
-
Cache Miss vs Hit:
- First visit to any page shows "loading..." (cache miss)
- Back/forward to visited pages is instant (cache hit)
- Even slow pages are instant on second visit
-
Server Actions:
- Go to "Counter Page" and increment server counter
- Notice the cache updates for current entry
- Navigate away and back to see updated state
-
Scroll Restoration: Browser handles this automatically via proper history state
This pattern is inspired by:
- Navigation API - Modern navigation standard
- hi-ogawa/vite-environment-examples - Back/forward cache implementation
- TanStack Router - History state key pattern
- React useTransition
- React.use
- GitHub Issue: #860
- Reproduction: https://github.com/hi-ogawa/reproductions/tree/main/vite-rsc-coordinate-history-and-transition