@@ -5,7 +5,7 @@ import { RouterProvider } from "../components";
55import { RSCRouterContext } from "../context" ;
66import { FrameworkContext , setIsHydrated } from "../dom/ssr/components" ;
77import type { FrameworkContextObject } from "../dom/ssr/entry" ;
8- import { createBrowserHistory , invariant } from "../router/history" ;
8+ import { createBrowserHistory , createPath , invariant } from "../router/history" ;
99import type { Router as DataRouter , RouterInit } from "../router/router" ;
1010import {
1111 createRouter ,
@@ -68,6 +68,8 @@ type WindowWithRouterGlobals = Window &
6868 __routerActionID : number ;
6969 } ;
7070
71+ const RSC_RELOAD_DOCUMENT_STORAGE_KEY = "react-router-rsc-reload-document" ;
72+
7173/**
7274 * Create a React `callServer` implementation for React Router.
7375 *
@@ -123,23 +125,30 @@ export function createCallServer({
123125 ( globalVar . __routerActionID ??= 0 ) + 1 ) ;
124126
125127 const temporaryReferences = createTemporaryReferenceSet ( ) ;
126- const payloadPromise = fetchImplementation (
127- new Request ( location . href , {
128- body : await encodeReply ( args , { temporaryReferences } ) ,
129- method : "POST" ,
130- headers : {
131- Accept : "text/x-component" ,
132- "rsc-action-id" : id ,
133- } ,
134- } ) ,
135- ) . then ( ( response ) => {
136- if ( ! response . body ) {
137- throw new Error ( "No response body" ) ;
138- }
139- return createFromReadableStream ( response . body , {
140- temporaryReferences,
141- } ) as Promise < RSCPayload > ;
128+ const request = new Request ( location . href , {
129+ body : await encodeReply ( args , { temporaryReferences } ) ,
130+ method : "POST" ,
131+ headers : {
132+ Accept : "text/x-component" ,
133+ "rsc-action-id" : id ,
134+ } ,
142135 } ) ;
136+ const payloadPromise = fetchImplementation ( request ) . then (
137+ async ( response ) => {
138+ if ( isRSCReloadDocumentResponse ( response ) ) {
139+ await reloadRSCResponseDocument ( getRSCReloadDocumentTarget ( request ) ) ;
140+ }
141+
142+ clearRSCReloadDocumentAttempt ( ) ;
143+
144+ if ( ! response . body ) {
145+ throw new Error ( "No response body" ) ;
146+ }
147+ return createFromReadableStream ( response . body , {
148+ temporaryReferences,
149+ } ) as Promise < RSCPayload > ;
150+ } ,
151+ ) ;
143152
144153 React . startTransition ( ( ) =>
145154 // @ts -expect-error - Needs React 19 types
@@ -301,7 +310,7 @@ function createRouterFromPayload({
301310 basename : payload . basename ,
302311 isSpaMode : false ,
303312 } ) ,
304- async patchRoutesOnNavigation ( { path, signal } ) {
313+ async patchRoutesOnNavigation ( { path, signal, fetcherKey } ) {
305314 if ( payload . routeDiscovery . mode === "initial" ) {
306315 if ( ! applyPatchesPromise ) {
307316 applyPatchesPromise = ( async ( ) => {
@@ -334,6 +343,7 @@ function createRouterFromPayload({
334343 createFromReadableStream ,
335344 fetchImplementation ,
336345 signal ,
346+ getRSCManifestReloadDocumentTarget ( path , fetcherKey ) ,
337347 ) ;
338348 } ,
339349 // FIXME: Pass `build.ssr` into this function
@@ -560,6 +570,12 @@ function getFetchAndDecodeViaRSC(
560570 new Request ( url , await createRequestInit ( request ) ) ,
561571 ) ;
562572
573+ if ( isRSCReloadDocumentResponse ( res ) ) {
574+ await reloadRSCResponseDocument ( getRSCReloadDocumentTarget ( request ) ) ;
575+ }
576+
577+ clearRSCReloadDocumentAttempt ( ) ;
578+
563579 // If this error'd without hitting the running server, then bubble a normal
564580 // `ErrorResponse` and don't try to decode the body with `turbo-stream`.
565581 //
@@ -1039,6 +1055,7 @@ async function fetchAndApplyManifestPatches(
10391055 createFromReadableStream : BrowserCreateFromReadableStreamFunction ,
10401056 fetchImplementation : ( request : Request ) => Promise < Response > ,
10411057 signal ?: AbortSignal ,
1058+ reloadDocumentPath ?: string ,
10421059) {
10431060 let url = getManifestUrl ( paths ) ;
10441061 if ( url == null ) {
@@ -1054,6 +1071,19 @@ async function fetchAndApplyManifestPatches(
10541071 }
10551072
10561073 let response = await fetchImplementation ( new Request ( url , { signal } ) ) ;
1074+ if ( isRSCReloadDocumentResponse ( response ) ) {
1075+ if ( reloadDocumentPath ) {
1076+ await reloadRSCResponseDocument ( reloadDocumentPath ) ;
1077+ }
1078+ console . warn (
1079+ "Detected an RSC manifest version mismatch during eager route discovery. " +
1080+ "The next navigation to an undiscovered route will reload the document." ,
1081+ ) ;
1082+ return ;
1083+ }
1084+
1085+ clearRSCReloadDocumentAttempt ( ) ;
1086+
10571087 if ( ! response . body || response . status < 200 || response . status >= 300 ) {
10581088 throw new Error ( "Unable to fetch new route matches from the server" ) ;
10591089 }
@@ -1090,6 +1120,74 @@ function addToFifoQueue(path: string, queue: Set<string>) {
10901120 queue . add ( path ) ;
10911121}
10921122
1123+ export function isRSCReloadDocumentResponse ( response : Response ) : boolean {
1124+ return (
1125+ response . status === 204 && response . headers . has ( "X-Remix-Reload-Document" )
1126+ ) ;
1127+ }
1128+
1129+ export function getRSCReloadDocumentTarget ( request : Request ) : string {
1130+ if ( request . method === "GET" ) {
1131+ return new URL ( request . url , window . location . href ) . toString ( ) ;
1132+ }
1133+
1134+ return window . location . href ;
1135+ }
1136+
1137+ export function getRSCManifestReloadDocumentTarget (
1138+ path : string ,
1139+ fetcherKey ?: string ,
1140+ ) : string {
1141+ if ( fetcherKey ) {
1142+ return window . location . href ;
1143+ }
1144+
1145+ let navigation = ( window as WindowWithRouterGlobals ) . __reactRouterDataRouter
1146+ ?. state . navigation ;
1147+ if ( navigation ?. location ) {
1148+ return createPath ( navigation . location ) ;
1149+ }
1150+
1151+ return path ;
1152+ }
1153+
1154+ export function claimRSCReloadDocumentAttempt ( target : string ) : boolean {
1155+ let href = new URL ( target , window . location . href ) . toString ( ) ;
1156+ try {
1157+ if ( sessionStorage . getItem ( RSC_RELOAD_DOCUMENT_STORAGE_KEY ) === href ) {
1158+ return false ;
1159+ }
1160+ sessionStorage . setItem ( RSC_RELOAD_DOCUMENT_STORAGE_KEY , href ) ;
1161+ } catch {
1162+ // If sessionStorage is unavailable, still attempt the reload.
1163+ }
1164+ return true ;
1165+ }
1166+
1167+ export function clearRSCReloadDocumentAttempt ( ) : void {
1168+ try {
1169+ sessionStorage . removeItem ( RSC_RELOAD_DOCUMENT_STORAGE_KEY ) ;
1170+ } catch {
1171+ // Session storage unavailable
1172+ }
1173+ }
1174+
1175+ async function reloadRSCResponseDocument ( target : string ) : Promise < never > {
1176+ let href = new URL ( target , window . location . href ) . toString ( ) ;
1177+ if ( ! claimRSCReloadDocumentAttempt ( href ) ) {
1178+ console . error ( "Unable to reload document due to RSC version mismatch." ) ;
1179+ throw new Error ( "Unable to reload document due to RSC version mismatch." ) ;
1180+ }
1181+
1182+ window . location . assign ( href ) ;
1183+ console . warn ( "Detected RSC version mismatch, reloading..." ) ;
1184+
1185+ // Stall here and let the browser reload, avoiding an ErrorBoundary flash.
1186+ return await new Promise < never > ( ( ) => {
1187+ // Intentionally never resolves.
1188+ } ) ;
1189+ }
1190+
10931191// Thanks Josh!
10941192// https://www.joshwcomeau.com/snippets/javascript/debounce/
10951193function debounce ( callback : ( ...args : unknown [ ] ) => unknown , wait : number ) {
0 commit comments