Skip to content

Commit 5cde395

Browse files
committed
feat: attempt at enabling full transition support for the rsc router
1 parent ebf2334 commit 5cde395

File tree

2 files changed

+164
-37
lines changed

2 files changed

+164
-37
lines changed

packages/react-router/lib/components.tsx

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,137 @@ export interface RouterProviderProps {
291291
unstable_onError?: unstable_ClientOnErrorFunction;
292292
}
293293

294+
function shallowDiff(a: any, b: any) {
295+
if (a === b) {
296+
return false;
297+
}
298+
let aKeys = Object.keys(a);
299+
let bKeys = Object.keys(b);
300+
if (aKeys.length !== bKeys.length) {
301+
return true;
302+
}
303+
for (let key of aKeys) {
304+
if (a[key] !== b[key]) {
305+
return true;
306+
}
307+
}
308+
return false;
309+
}
310+
311+
export function UNSTABLE_TransitionEnabledRouterProvider({
312+
router,
313+
flushSync: reactDomFlushSyncImpl,
314+
unstable_onError,
315+
}: RouterProviderProps) {
316+
let fetcherData = React.useRef<Map<string, any>>(new Map());
317+
let [revalidating, startRevalidation] = React.useTransition();
318+
(router as any).__startRevalidation = startRevalidation;
319+
let [_state, setState] = React.useState(router.state);
320+
let [state, setOptimisticState] = (
321+
(React as any).useOptimistic as typeof React.useState
322+
)(_state);
323+
324+
let navigator = React.useMemo((): Navigator => {
325+
return {
326+
createHref: router.createHref,
327+
encodeLocation: router.encodeLocation,
328+
go: (n) => router.navigate(n),
329+
push: (to, state, opts) =>
330+
router.navigate(to, {
331+
state,
332+
preventScrollReset: opts?.preventScrollReset,
333+
}),
334+
replace: (to, state, opts) =>
335+
router.navigate(to, {
336+
replace: true,
337+
state,
338+
preventScrollReset: opts?.preventScrollReset,
339+
}),
340+
};
341+
}, [router]);
342+
343+
let basename = router.basename || "/";
344+
345+
let dataRouterContext = React.useMemo(
346+
() => ({
347+
router,
348+
navigator,
349+
static: false,
350+
basename,
351+
unstable_onError,
352+
}),
353+
[router, navigator, basename, unstable_onError],
354+
);
355+
356+
React.useLayoutEffect(() => {
357+
return router.subscribe(
358+
(newState, { deletedFetchers, flushSync, viewTransitionOpts }) => {
359+
newState.fetchers.forEach((fetcher, key) => {
360+
if (fetcher.data !== undefined) {
361+
fetcherData.current.set(key, fetcher.data);
362+
}
363+
});
364+
deletedFetchers.forEach((key) => fetcherData.current.delete(key));
365+
366+
const diff = shallowDiff(state, newState);
367+
368+
if (!diff) return;
369+
370+
if (flushSync) {
371+
if (reactDomFlushSyncImpl) {
372+
reactDomFlushSyncImpl(() => setState(newState));
373+
} else {
374+
setState(newState);
375+
}
376+
} else {
377+
React.startTransition(() => {
378+
setOptimisticState(newState);
379+
setState(newState);
380+
});
381+
}
382+
},
383+
);
384+
}, [router, reactDomFlushSyncImpl, state, setOptimisticState]);
385+
386+
// The fragment and {null} here are important! We need them to keep React 18's
387+
// useId happy when we are server-rendering since we may have a <script> here
388+
// containing the hydrated server-side staticContext (from StaticRouterProvider).
389+
// useId relies on the component tree structure to generate deterministic id's
390+
// so we need to ensure it remains the same on the client even though
391+
// we don't need the <script> tag
392+
return (
393+
<>
394+
<DataRouterContext.Provider value={dataRouterContext}>
395+
<DataRouterStateContext.Provider
396+
value={{
397+
...state,
398+
revalidation: revalidating ? "loading" : state.revalidation,
399+
}}
400+
>
401+
<FetchersContext.Provider value={fetcherData.current}>
402+
{/* <ViewTransitionContext.Provider value={vtContext}> */}
403+
<Router
404+
basename={basename}
405+
location={state.location}
406+
navigationType={state.historyAction}
407+
navigator={navigator}
408+
>
409+
<MemoizedDataRoutes
410+
routes={router.routes}
411+
future={router.future}
412+
state={state}
413+
unstable_onError={unstable_onError}
414+
/>
415+
</Router>
416+
{/* </ViewTransitionContext.Provider> */}
417+
</FetchersContext.Provider>
418+
</DataRouterStateContext.Provider>
419+
</DataRouterContext.Provider>
420+
{null}
421+
</>
422+
);
423+
}
424+
294425
/**
295426
* Render the UI for the given {@link DataRouter}. This component should
296427
* typically be at the top of an app's element tree.

packages/react-router/lib/rsc/browser.tsx

Lines changed: 33 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as React from "react";
22
import * as ReactDOM from "react-dom";
33

4-
import { RouterProvider } from "../components";
4+
import { UNSTABLE_TransitionEnabledRouterProvider as RouterProvider } from "../components";
55
import {
66
RSCRouterContext,
77
type DataRouteMatch,
@@ -153,8 +153,7 @@ export function createCallServer({
153153
}
154154

155155
if (payload.rerender) {
156-
React.startTransition(
157-
// @ts-expect-error - We have old react types that don't know this can be async
156+
(globalVar.__reactRouterDataRouter as any).__startRevalidation(
158157
async () => {
159158
const rerender = await payload.rerender;
160159
if (!rerender) return;
@@ -176,41 +175,38 @@ export function createCallServer({
176175
return;
177176
}
178177

179-
let lastMatch: RSCRouteManifest | undefined;
180-
for (const match of rerender.matches) {
181-
globalVar.__reactRouterDataRouter.patchRoutes(
182-
lastMatch?.id ?? null,
183-
[createRouteFromServerManifest(match)],
184-
true,
185-
);
186-
lastMatch = match;
187-
}
188-
(
189-
window as WindowWithRouterGlobals
190-
).__reactRouterDataRouter._internalSetStateDoNotUseOrYouWillBreakYourApp(
191-
{},
192-
);
178+
(globalVar.__reactRouterDataRouter as any).__startRevalidation(
179+
() => {
180+
let lastMatch: RSCRouteManifest | undefined;
181+
for (const match of rerender.matches) {
182+
globalVar.__reactRouterDataRouter.patchRoutes(
183+
lastMatch?.id ?? null,
184+
[createRouteFromServerManifest(match)],
185+
true,
186+
);
187+
lastMatch = match;
188+
}
193189

194-
React.startTransition(() => {
195-
(
196-
window as WindowWithRouterGlobals
197-
).__reactRouterDataRouter._internalSetStateDoNotUseOrYouWillBreakYourApp(
198-
{
199-
loaderData: Object.assign(
200-
{},
201-
globalVar.__reactRouterDataRouter.state.loaderData,
202-
rerender.loaderData,
203-
),
204-
errors: rerender.errors
205-
? Object.assign(
206-
{},
207-
globalVar.__reactRouterDataRouter.state.errors,
208-
rerender.errors,
209-
)
210-
: null,
211-
},
212-
);
213-
});
190+
(
191+
window as WindowWithRouterGlobals
192+
).__reactRouterDataRouter._internalSetStateDoNotUseOrYouWillBreakYourApp(
193+
{
194+
loaderData: Object.assign(
195+
{},
196+
globalVar.__reactRouterDataRouter.state.loaderData,
197+
rerender.loaderData,
198+
),
199+
errors: rerender.errors
200+
? Object.assign(
201+
{},
202+
globalVar.__reactRouterDataRouter.state.errors,
203+
rerender.errors,
204+
)
205+
: null,
206+
},
207+
);
208+
},
209+
);
214210
}
215211
},
216212
);

0 commit comments

Comments
 (0)