Skip to content

Commit

Permalink
POC of TransitionProvider approach
Browse files Browse the repository at this point in the history
  • Loading branch information
brophdawg11 committed Oct 13, 2023
1 parent 83b5157 commit 4cff152
Show file tree
Hide file tree
Showing 9 changed files with 83 additions and 110 deletions.
13 changes: 11 additions & 2 deletions examples/view-transitions/src/main.tsx
Expand Up @@ -10,6 +10,7 @@ import {
NavLink,
Outlet,
RouterProvider,
TransitionProvider,
unstable_useViewTransitionState,
useActionData,
useLoaderData,
Expand Down Expand Up @@ -246,13 +247,21 @@ function NavImage({ src, idx }: { src: string; idx: number }) {
const rootElement = document.getElementById("root") as HTMLElement;
ReactDOMClient.createRoot(rootElement).render(
<React.StrictMode>
<RouterProvider
<TransitionProvider
router={router}
future={{
// Wrap all state updates in React.startTransition()
v7_startTransition: true,
}}
/>
>
<RouterProvider
router={router}
future={{
// Wrap all state updates in React.startTransition()
v7_startTransition: true,
}}
/>
</TransitionProvider>
</React.StrictMode>
);

Expand Down
Expand Up @@ -34,6 +34,7 @@ import {
useMatches,
useSearchParams,
createRoutesFromElements,
TransitionProvider,
} from "react-router-dom";

import createDeferred from "../../router/__tests__/utils/createDeferred";
Expand Down Expand Up @@ -6006,7 +6007,11 @@ function testDomRouter(
],
{ window: testWindow }
);
render(<RouterProvider router={router} />);
render(
<TransitionProvider router={router}>
<RouterProvider router={router} />
</TransitionProvider>
);

expect(screen.getByText("Home")).toBeDefined();
fireEvent.click(screen.getByText("/a"));
Expand Down
8 changes: 1 addition & 7 deletions packages/react-router-dom/__tests__/exports-test.tsx
Expand Up @@ -4,21 +4,15 @@ import * as ReactRouterDOM from "react-router-dom";
let nonReExportedKeys = new Set([
"UNSAFE_mapRouteProperties",
"UNSAFE_useRoutesImpl",
"UNSAFE_DataRouterSubscriberContext",
]);

let modifiedExports = new Set(["RouterProvider"]);

describe("react-router-dom", () => {
for (let key in ReactRouter) {
if (nonReExportedKeys.has(key)) {
it(`does not re-export ${key} from react-router`, () => {
expect(ReactRouterDOM[key]).toBe(undefined);
});
} else if (modifiedExports.has(key)) {
it(`re-exports a different version of ${key}`, () => {
expect(ReactRouterDOM[key]).toBeDefined();
expect(ReactRouterDOM[key]).not.toBe(ReactRouter[key]);
});
} else {
it(`re-exports ${key} from react-router`, () => {
expect(ReactRouterDOM[key]).toBe(ReactRouter[key]);
Expand Down
97 changes: 17 additions & 80 deletions packages/react-router-dom/index.tsx
Expand Up @@ -4,15 +4,12 @@
*/
import * as React from "react";
import type {
DataRouteObject,
FutureConfig,
Location,
NavigateOptions,
NavigationType,
Navigator,
RelativeRoutingType,
RouteObject,
RouterProviderProps,
To,
} from "react-router";
import {
Expand All @@ -31,7 +28,7 @@ import {
UNSAFE_RouteContext as RouteContext,
UNSAFE_mapRouteProperties as mapRouteProperties,
UNSAFE_useRouteId as useRouteId,
UNSAFE_useRoutesImpl as useRoutesImpl,
UNSAFE_DataRouterSubscriberContext as DataRouterSubscriberContext,
} from "react-router";
import type {
BrowserHistory,
Expand Down Expand Up @@ -148,6 +145,7 @@ export {
Outlet,
Route,
Router,
RouterProvider,
Routes,
createMemoryRouter,
createPath,
Expand Down Expand Up @@ -419,14 +417,20 @@ class Deferred<T> {
}
}

interface TransitionProviderProps {
router: RemixRouter;
future?: Partial<FutureConfig>;
children: React.ReactNode | React.ReactNode[];
}

/**
* Given a Remix Router instance, render the appropriate UI
* Enable support for View Transitions in a RouterProvider
*/
export function RouterProvider({
fallbackElement,
export function TransitionProvider({
router,
future,
}: RouterProviderProps): React.ReactElement {
children,
}: TransitionProviderProps): React.ReactElement {
let [state, setStateImpl] = React.useState(router.state);
let [pendingState, setPendingState] = React.useState<RouterState>();
let [vtContext, setVtContext] = React.useState<ViewTransitionContextObject>({
Expand Down Expand Up @@ -487,10 +491,6 @@ export function RouterProvider({
[optInStartTransition, transition, renderDfd, router.window]
);

// Need to use a layout effect here so we are subscribed early enough to
// pick up on any render-driven redirects/navigations (useEffect/<Navigate>)
React.useLayoutEffect(() => router.subscribe(setState), [router, setState]);

// When we start a view transition, create a Deferred we can use for the
// eventual "completed" render
React.useEffect(() => {
Expand Down Expand Up @@ -546,78 +546,15 @@ export function RouterProvider({
}
}, [vtContext.isTransitioning, interruption]);

let navigator = React.useMemo((): Navigator => {
return {
createHref: router.createHref,
encodeLocation: router.encodeLocation,
go: (n) => router.navigate(n),
push: (to, state, opts) =>
router.navigate(to, {
state,
preventScrollReset: opts?.preventScrollReset,
}),
replace: (to, state, opts) =>
router.navigate(to, {
replace: true,
state,
preventScrollReset: opts?.preventScrollReset,
}),
};
}, [router]);

let basename = router.basename || "/";

let dataRouterContext = React.useMemo(
() => ({
router,
navigator,
static: false,
basename,
}),
[router, navigator, basename]
);

// The fragment and {null} here are important! We need them to keep React 18's
// useId happy when we are server-rendering since we may have a <script> here
// containing the hydrated server-side staticContext (from StaticRouterProvider).
// useId relies on the component tree structure to generate deterministic id's
// so we need to ensure it remains the same on the client even though
// we don't need the <script> tag
return (
<>
<DataRouterContext.Provider value={dataRouterContext}>
<DataRouterStateContext.Provider value={state}>
<ViewTransitionContext.Provider value={vtContext}>
<Router
basename={basename}
location={state.location}
navigationType={state.historyAction}
navigator={navigator}
>
{state.initialized ? (
<DataRoutes routes={router.routes} state={state} />
) : (
fallbackElement
)}
</Router>
</ViewTransitionContext.Provider>
</DataRouterStateContext.Provider>
</DataRouterContext.Provider>
{null}
</>
<DataRouterSubscriberContext.Provider value={[state, setState]}>
<ViewTransitionContext.Provider value={vtContext}>
{children}
</ViewTransitionContext.Provider>
</DataRouterSubscriberContext.Provider>
);
}

function DataRoutes({
routes,
state,
}: {
routes: DataRouteObject[];
state: RouterState;
}): React.ReactElement | null {
return useRoutesImpl(routes, undefined, state);
}

export interface BrowserRouterProps {
basename?: string;
children?: React.ReactNode;
Expand Down
33 changes: 18 additions & 15 deletions packages/react-router-dom/server.tsx
Expand Up @@ -19,6 +19,7 @@ import {
UNSAFE_convertRoutesToDataRoutes as convertRoutesToDataRoutes,
} from "@remix-run/router";
import {
UNSAFE_DataRouterSubscriberContext as DataRouterSubscriberContext,
UNSAFE_mapRouteProperties as mapRouteProperties,
UNSAFE_useRoutesImpl as useRoutesImpl,
} from "react-router";
Expand Down Expand Up @@ -130,21 +131,23 @@ export function StaticRouterProvider({

return (
<>
<DataRouterContext.Provider value={dataRouterContext}>
<DataRouterStateContext.Provider value={state}>
<ViewTransitionContext.Provider value={{ isTransitioning: false }}>
<Router
basename={dataRouterContext.basename}
location={state.location}
navigationType={state.historyAction}
navigator={dataRouterContext.navigator}
static={dataRouterContext.static}
>
<DataRoutes routes={router.routes} state={state} />
</Router>
</ViewTransitionContext.Provider>
</DataRouterStateContext.Provider>
</DataRouterContext.Provider>
<DataRouterSubscriberContext.Provider value={null}>
<ViewTransitionContext.Provider value={{ isTransitioning: false }}>
<DataRouterContext.Provider value={dataRouterContext}>
<DataRouterStateContext.Provider value={state}>
<Router
basename={dataRouterContext.basename}
location={state.location}
navigationType={state.historyAction}
navigator={dataRouterContext.navigator}
static={dataRouterContext.static}
>
<DataRoutes routes={router.routes} state={state} />
</Router>
</DataRouterStateContext.Provider>
</DataRouterContext.Provider>
</ViewTransitionContext.Provider>
</DataRouterSubscriberContext.Provider>
{hydrateScript ? (
<script
suppressHydrationWarning
Expand Down
1 change: 1 addition & 0 deletions packages/react-router-native/__tests__/exports-test.tsx
Expand Up @@ -4,6 +4,7 @@ import * as ReactRouterNative from "react-router-native";
let nonReExportedKeys = new Set([
"UNSAFE_mapRouteProperties",
"UNSAFE_useRoutesImpl",
"UNSAFE_DataRouterSubscriberContext",
"UNSAFE_ViewTransitionContext",
]);

Expand Down
2 changes: 2 additions & 0 deletions packages/react-router/index.ts
Expand Up @@ -86,6 +86,7 @@ import type {
import {
DataRouterContext,
DataRouterStateContext,
DataRouterSubscriberContext,
LocationContext,
NavigationContext,
RouteContext,
Expand Down Expand Up @@ -305,6 +306,7 @@ export function createMemoryRouter(
export {
DataRouterContext as UNSAFE_DataRouterContext,
DataRouterStateContext as UNSAFE_DataRouterStateContext,
DataRouterSubscriberContext as UNSAFE_DataRouterSubscriberContext,
LocationContext as UNSAFE_LocationContext,
NavigationContext as UNSAFE_NavigationContext,
RouteContext as UNSAFE_RouteContext,
Expand Down
23 changes: 18 additions & 5 deletions packages/react-router/lib/components.tsx
Expand Up @@ -35,6 +35,7 @@ import {
AwaitContext,
DataRouterContext,
DataRouterStateContext,
DataRouterSubscriberContext,
LocationContext,
NavigationContext,
RouteContext,
Expand Down Expand Up @@ -92,23 +93,35 @@ export function RouterProvider({
router,
future,
}: RouterProviderProps): React.ReactElement {
let [state, setStateImpl] = React.useState(router.state);
let ctx = React.useContext(DataRouterSubscriberContext);
let ctxState = ctx ? ctx[0] : null;
let ctxSetState = ctx ? ctx[1] : null;
let [localState, localSetStateImpl] = React.useState(router.state);
let { v7_startTransition } = future || {};

let setState = React.useCallback<RouterSubscriber>(
(newState: RouterState) => {
if (v7_startTransition && startTransitionImpl) {
startTransitionImpl(() => setStateImpl(newState));
startTransitionImpl(() => localSetStateImpl(newState));
} else {
setStateImpl(newState);
localSetStateImpl(newState);
}
},
[setStateImpl, v7_startTransition]
[localSetStateImpl, v7_startTransition]
);

// If we're inside a DataRouterSubscriberContext that needs fine-grained
// control of the state updates (for startViewTransition), then prefer those
// over our own state/setStateImpl
let state = ctxState || localState;
let subscriber = ctxSetState || setState;

// Need to use a layout effect here so we are subscribed early enough to
// pick up on any render-driven redirects/navigations (useEffect/<Navigate>)
React.useLayoutEffect(() => router.subscribe(setState), [router, setState]);
React.useLayoutEffect(
() => router.subscribe(subscriber),
[router, subscriber]
);

let navigator = React.useMemo((): Navigator => {
return {
Expand Down
9 changes: 9 additions & 0 deletions packages/react-router/lib/context.ts
Expand Up @@ -9,6 +9,8 @@ import type {
Action as NavigationType,
RelativeRoutingType,
Router,
RouterState,
RouterSubscriber,
StaticHandlerContext,
To,
TrackedPromise,
Expand Down Expand Up @@ -84,6 +86,13 @@ if (__DEV__) {
DataRouterStateContext.displayName = "DataRouterState";
}

export const DataRouterSubscriberContext = React.createContext<
[RouterState, RouterSubscriber] | null
>(null);
if (__DEV__) {
DataRouterSubscriberContext.displayName = "DataRouterSubscriber";
}

export const AwaitContext = React.createContext<TrackedPromise | null>(null);
if (__DEV__) {
AwaitContext.displayName = "Await";
Expand Down

0 comments on commit 4cff152

Please sign in to comment.