-
-
Notifications
You must be signed in to change notification settings - Fork 238
Expand file tree
/
Copy pathentry.browser.tsx
More file actions
138 lines (122 loc) · 4.11 KB
/
entry.browser.tsx
File metadata and controls
138 lines (122 loc) · 4.11 KB
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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
import {
createFromReadableStream,
createFromFetch,
setServerCallback,
createTemporaryReferenceSet,
encodeReply,
} from '@vitejs/plugin-rsc/browser'
import React from 'react'
import { createRoot, hydrateRoot } from 'react-dom/client'
import { rscStream } from 'rsc-html-stream/client'
import type { RscPayload } from './entry.rsc'
import { GlobalErrorBoundary } from './error-boundary'
import { createRscRenderRequest } from './request'
async function main() {
// stash `setPayload` function to trigger re-rendering
// from outside of `BrowserRoot` component (e.g. server function call, navigation, hmr)
let setPayload: (v: RscPayload) => void
// deserialize RSC stream back to React VDOM for CSR
const initialPayload = await createFromReadableStream<RscPayload>(
// initial RSC stream is injected in SSR stream as <script>...FLIGHT_DATA...</script>
rscStream,
)
// browser root component to (re-)render RSC payload as state
function BrowserRoot() {
const [payload, setPayload_] = React.useState(initialPayload)
React.useEffect(() => {
setPayload = (v) => React.startTransition(() => setPayload_(v))
}, [setPayload_])
// re-fetch/render on client side navigation
React.useEffect(() => {
return listenNavigation(() => fetchRscPayload())
}, [])
return payload.root
}
// re-fetch RSC and trigger re-rendering
async function fetchRscPayload() {
const renderRequest = createRscRenderRequest(window.location.href)
const payload = await createFromFetch<RscPayload>(fetch(renderRequest))
setPayload(payload)
}
// register a handler which will be internally called by React
// on server function request after hydration.
setServerCallback(async (id, args) => {
const temporaryReferences = createTemporaryReferenceSet()
const renderRequest = createRscRenderRequest(window.location.href, {
id,
body: await encodeReply(args, { temporaryReferences }),
})
const payload = await createFromFetch<RscPayload>(fetch(renderRequest), {
temporaryReferences,
})
setPayload(payload)
const { ok, data } = payload.returnValue!
if (!ok) throw data
return data
})
// hydration
const browserRoot = (
<React.StrictMode>
<GlobalErrorBoundary>
<BrowserRoot />
</GlobalErrorBoundary>
</React.StrictMode>
)
if ('__NO_HYDRATE' in globalThis) {
createRoot(document).render(browserRoot)
} else {
hydrateRoot(document, browserRoot, {
formState: initialPayload.formState,
})
}
// implement server HMR by triggering re-fetch/render of RSC upon server code change
if (import.meta.hot) {
import.meta.hot.on('rsc:update', () => {
fetchRscPayload()
})
}
}
// a little helper to setup events interception for client side navigation
function listenNavigation(onNavigation: () => 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
}
}
main()