1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109import {
createFromFetch,
createFromReadableStream,
} from '@vitejs/plugin-rsc/browser'
import React from 'react'
import { hydrateRoot } from 'react-dom/client'
import { rscStream } from 'rsc-html-stream/client'
import { RSC_POSTFIX, type RscPayload } from './shared'
/**
* Hydrates the React app on the client using the initial RSC payload and sets up navigation listeners.
*/
async function hydrate(): Promise<void> {
async function onNavigation() {
const url = new URL(window.location.href)
url.pathname = url.pathname + RSC_POSTFIX
const payload = await createFromFetch<RscPayload>(fetch(url))
setPayload(payload)
}
const initialPayload = await createFromReadableStream<RscPayload>(rscStream)
let setPayload: (v: RscPayload) => void
function BrowserRoot() {
const [payload, setPayload_] = React.useState(initialPayload)
React.useEffect(() => {
setPayload = (v) => React.startTransition(() => setPayload_(v))
}, [setPayload_])
React.useEffect(() => {
return listenNavigation(() => onNavigation())
}, [])
return payload.root
}
const browserRoot = (
<React.StrictMode>
<BrowserRoot />
</React.StrictMode>
)
hydrateRoot(document, browserRoot)
if (import.meta.hot) {
import.meta.hot.on('rsc:update', () => {
window.history.replaceState({}, '', window.location.href)
})
}
}
/**
* Sets up navigation event listeners for SPA-style routing (pushState, replaceState, popstate, anchor clicks).
* Calls the provided callback on navigation events.
*
* @param onNavigation - Callback to run on navigation
* @returns Cleanup function to remove all listeners
*/
function listenNavigation(onNavigation: () => void): () => void {
window.addEventListener('popstate', onNavigation)
const oldPushState = window.history.pushState
window.history.pushState = function (...args) {
const res = oldPushState.apply(this, args)
onNavigation()
return res
}
const oldReplaceState = window.history.replaceState
window.history.replaceState = function (...args) {
const res = oldReplaceState.apply(this, args)
onNavigation()
return res
}
function onClick(e: MouseEvent) {
let link = (e.target as Element).closest('a')
if (
link &&
link instanceof HTMLAnchorElement &&
link.href &&
(!link.target || link.target === '_self') &&
link.origin === location.origin &&
!link.hasAttribute('download') &&
e.button === 0 && // left clicks only
!e.metaKey && // open in new tab (mac)
!e.ctrlKey && // open in new tab (windows)
!e.altKey && // download
!e.shiftKey &&
!e.defaultPrevented
) {
e.preventDefault()
history.pushState(null, '', link.href)
}
}
document.addEventListener('click', onClick)
return () => {
document.removeEventListener('click', onClick)
window.removeEventListener('popstate', onNavigation)
window.history.pushState = oldPushState
window.history.replaceState = oldReplaceState
}
}
hydrate()