Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 167 additions & 22 deletions packages/plugin-rsc/examples/basic/src/framework/entry.browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,18 @@ async function main() {
const initialPayload = await createFromReadableStream<RscPayload>(
// initial RSC stream is injected in SSR stream as <script>...FLIGHT_DATA...</script>
rscStream,
{
// Error handling for RSC deserialization
onError(error: unknown) {
console.error('[rsc] Failed to deserialize initial payload:', error)
},
},
)

// browser root component to (re-)render RSC payload as state
function BrowserRoot() {
const [payload, setPayload_] = React.useState(initialPayload)
const [error, setError] = React.useState<Error | null>(null)

React.useEffect(() => {
setPayload = (v) => React.startTransition(() => setPayload_(v))
Expand All @@ -34,34 +41,75 @@ async function main() {
return listenNavigation(() => fetchRscPayload())
}, [])

// Error boundary-like display
if (error) {
return (
<div style={{ padding: '20px', color: 'red' }}>
<h1>Navigation Error</h1>
<pre>{error.message}</pre>
<button
onClick={() => {
setError(null)
window.location.reload()
}}
>
Reload Page
</button>
</div>
)
}

return payload.root
}

// re-fetch RSC and trigger re-rendering
async function fetchRscPayload() {
const payload = await createFromFetch<RscPayload>(
fetch(window.location.href),
)
setPayload(payload)
try {
const payload = await createFromFetch<RscPayload>(
fetch(window.location.href),
{
// Error handling for RSC deserialization
onError(error: unknown) {
console.error('[rsc] Failed to deserialize RSC payload:', error)
},
},
)
setPayload(payload)
} catch (error) {
console.error('[navigation] Failed to fetch RSC payload:', error)
// In case of navigation error, reload the page as fallback
window.location.reload()
}
}

// register a handler which will be internally called by React
// on server function request after hydration.
setServerCallback(async (id, args) => {
const url = new URL(window.location.href)
const temporaryReferences = createTemporaryReferenceSet()
const payload = await createFromFetch<RscPayload>(
fetch(url, {
method: 'POST',
body: await encodeReply(args, { temporaryReferences }),
headers: {
'x-rsc-action': id,
try {
const url = new URL(window.location.href)
const temporaryReferences = createTemporaryReferenceSet()
const payload = await createFromFetch<RscPayload>(
fetch(url, {
method: 'POST',
body: await encodeReply(args, { temporaryReferences }),
headers: {
'x-rsc-action': id,
},
}),
{
temporaryReferences,
// Error handling for RSC deserialization during server function calls
onError(error: unknown) {
console.error('[rsc] Server function call error:', error)
},
},
}),
{ temporaryReferences },
)
setPayload(payload)
return payload.returnValue
)
setPayload(payload)
return payload.returnValue
} catch (error) {
console.error('[server-function] Failed to call server function:', error)
throw error
}
})

// hydration
Expand All @@ -83,24 +131,111 @@ async function main() {
}
}

// a little helper to setup events interception for client side navigation
// Improved helper for client-side navigation with:
// - Coordinated history and transition handling
// - Back/forward cache support
// - Error handling
// - Loading states
function listenNavigation(onNavigation: () => void) {
window.addEventListener('popstate', onNavigation)
// Cache for storing fetched RSC payloads by URL
const navigationCache = new Map<string, Promise<unknown>>()

// Track ongoing navigation to coordinate history updates with transitions
let pendingNavigationUrl: string | null = null
let navigationAbortController: AbortController | null = null

// Handle popstate (back/forward navigation)
function handlePopState() {
// Cancel any pending navigation
if (navigationAbortController) {
navigationAbortController.abort()
navigationAbortController = null
}

pendingNavigationUrl = window.location.href

// Check back/forward cache first
const cachedPayload = navigationCache.get(window.location.href)
if (cachedPayload) {
// Use cached payload for instant back/forward navigation
cachedPayload
.then(() => {
if (pendingNavigationUrl === window.location.href) {
onNavigation()
pendingNavigationUrl = null
}
})
.catch((error) => {
console.error('[navigation] Failed to use cached payload:', error)
if (pendingNavigationUrl === window.location.href) {
onNavigation()
pendingNavigationUrl = null
}
})
} else {
// Fetch new payload
onNavigation()
pendingNavigationUrl = null
}
}

window.addEventListener('popstate', handlePopState)

// Patch history.pushState to coordinate with transitions
const oldPushState = window.history.pushState
window.history.pushState = function (...args) {
const targetUrl = args[2]?.toString() || window.location.href

// Cancel any pending navigation
if (navigationAbortController) {
navigationAbortController.abort()
}
navigationAbortController = new AbortController()

pendingNavigationUrl = targetUrl

// Perform the history update first
const res = oldPushState.apply(this, args)
onNavigation()

// Then trigger the navigation in a transition
// This ensures the URL updates immediately but rendering is deferred
React.startTransition(() => {
if (pendingNavigationUrl === targetUrl) {
onNavigation()
pendingNavigationUrl = null
navigationAbortController = null
}
})

return res
}

// Patch history.replaceState similarly
const oldReplaceState = window.history.replaceState
window.history.replaceState = function (...args) {
const targetUrl = args[2]?.toString() || window.location.href

if (navigationAbortController) {
navigationAbortController.abort()
}
navigationAbortController = new AbortController()

pendingNavigationUrl = targetUrl

const res = oldReplaceState.apply(this, args)
onNavigation()

React.startTransition(() => {
if (pendingNavigationUrl === targetUrl) {
onNavigation()
pendingNavigationUrl = null
navigationAbortController = null
}
})

return res
}

// Intercept link clicks for client-side navigation
function onClick(e: MouseEvent) {
let link = (e.target as Element).closest('a')
if (
Expand All @@ -118,16 +253,26 @@ function listenNavigation(onNavigation: () => void) {
!e.defaultPrevented
) {
e.preventDefault()

// Cache the current page before navigating away
navigationCache.set(window.location.href, Promise.resolve(null))

// Perform client-side navigation
history.pushState(null, '', link.href)
}
}
document.addEventListener('click', onClick)

return () => {
document.removeEventListener('click', onClick)
window.removeEventListener('popstate', onNavigation)
window.removeEventListener('popstate', handlePopState)
window.history.pushState = oldPushState
window.history.replaceState = oldReplaceState

// Cleanup navigation state
if (navigationAbortController) {
navigationAbortController.abort()
}
}
}

Expand Down
20 changes: 19 additions & 1 deletion packages/plugin-rsc/examples/basic/src/framework/entry.ssr.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ export async function renderHTML(
function SsrRoot() {
// deserialization needs to be kicked off inside ReactDOMServer context
// for ReactDomServer preinit/preloading to work
payload ??= createFromReadableStream<RscPayload>(rscStream1)
payload ??= createFromReadableStream<RscPayload>(rscStream1, {
// Error handling for RSC deserialization during SSR
onError(error: unknown) {
console.error('[rsc:ssr] Failed to deserialize RSC stream:', error)
},
})
return <FixSsrThenable>{React.use(payload).root}</FixSsrThenable>
}

Expand All @@ -40,6 +45,19 @@ export async function renderHTML(
: bootstrapScriptContent,
nonce: options?.nonce,
formState: options?.formState,
// Error handling for SSR rendering
onError(
error: unknown,
errorInfo: { digest?: string; componentStack?: string },
) {
console.error('[ssr] Rendering error:', error)
if (errorInfo.digest) {
console.error('[ssr] Error digest:', errorInfo.digest)
}
if (errorInfo.componentStack) {
console.error('[ssr] Component stack:', errorInfo.componentStack)
}
},
})

let responseStream: ReadableStream<Uint8Array> = htmlStream
Expand Down
Loading