๐Ÿ“ฆ noxify / vite-rsc-ssg-renoun

๐Ÿ“„ entry.browser.tsx ยท 109 lines
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()