Skip to content

Commit a252428

Browse files
authored
Reduce RouterProvider re-renders when using View Transitions (#11803)
1 parent 56d0b4b commit a252428

File tree

4 files changed

+96
-7
lines changed

4 files changed

+96
-7
lines changed

.changeset/fuzzy-worms-applaud.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router-dom": patch
3+
---
4+
5+
Memoize some `RouterProvider` internals to reduce uneccesary re-renders

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@
114114
"none": "17.3 kB"
115115
},
116116
"packages/react-router-dom/dist/react-router-dom.production.min.js": {
117-
"none": "17.2 kB"
117+
"none": "17.3 kB"
118118
},
119119
"packages/react-router-dom/dist/umd/react-router-dom.production.min.js": {
120120
"none": "23.6 kB"

packages/react-router-dom/__tests__/data-browser-router-test.tsx

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ErrorResponse, Fetcher } from "@remix-run/router";
1+
import type { ErrorResponse, Fetcher, RouterState } from "@remix-run/router";
22
import "@testing-library/jest-dom";
33
import {
44
act,
@@ -37,7 +37,7 @@ import {
3737
} from "react-router-dom";
3838

3939
import getHtml from "../../react-router/__tests__/utils/getHtml";
40-
import { createDeferred } from "../../router/__tests__/utils/utils";
40+
import { createDeferred, tick } from "../../router/__tests__/utils/utils";
4141

4242
testDomRouter("<DataBrowserRouter>", createBrowserRouter, (url) =>
4343
getWindowImpl(url, false)
@@ -7465,6 +7465,81 @@ function testDomRouter(
74657465
await waitFor(() => screen.getByText("D"));
74667466
expect(spy).toHaveBeenCalledTimes(2);
74677467
});
7468+
7469+
it("Does not cause extra re-renders due to ViewTransitionContext updates", async () => {
7470+
let testWindow = getWindow("/");
7471+
testWindow.document.startViewTransition = (cb) => {
7472+
cb();
7473+
return {
7474+
ready: Promise.resolve(),
7475+
finished: Promise.resolve(),
7476+
updateCallbackDone: Promise.resolve(),
7477+
skipTransition: () => {},
7478+
};
7479+
};
7480+
7481+
let renders: RouterState[] = [];
7482+
let router = createTestRouter(
7483+
[
7484+
{
7485+
path: "/",
7486+
Component() {
7487+
return (
7488+
<>
7489+
<Link to="/page" unstable_viewTransition>
7490+
/page
7491+
</Link>
7492+
<Outlet />
7493+
</>
7494+
);
7495+
},
7496+
children: [
7497+
{
7498+
index: true,
7499+
async loader() {
7500+
await tick();
7501+
return "INDEX";
7502+
},
7503+
Component() {
7504+
renders.push(useLocation(), useNavigation());
7505+
return <h1>{useLoaderData()}</h1>;
7506+
},
7507+
},
7508+
{
7509+
path: "page",
7510+
async loader() {
7511+
await tick();
7512+
return "PAGE";
7513+
},
7514+
Component() {
7515+
renders.push(useLocation(), useNavigation());
7516+
return <h1>{useLoaderData()}</h1>;
7517+
},
7518+
},
7519+
],
7520+
},
7521+
],
7522+
{ window: testWindow }
7523+
);
7524+
render(<RouterProvider router={router} />);
7525+
await waitFor(() => screen.getByText("INDEX"));
7526+
7527+
renders = [];
7528+
fireEvent.click(screen.getByText("/page"));
7529+
await waitFor(() => screen.getByText("PAGE"));
7530+
7531+
expect(renders).toMatchObject([
7532+
// Re-render of current location with navigation.state = "loading"
7533+
{ pathname: "/" },
7534+
{
7535+
state: "loading",
7536+
location: { pathname: "/page" },
7537+
},
7538+
// Render of new location with navigation.state = "idle"
7539+
{ pathname: "/page" },
7540+
{ state: "idle" },
7541+
]);
7542+
});
74687543
});
74697544
});
74707545
}

packages/react-router-dom/index.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
Navigator,
1414
RelativeRoutingType,
1515
RouteObject,
16+
RouterProps,
1617
RouterProviderProps,
1718
To,
1819
unstable_PatchRoutesOnMissFunction,
@@ -708,6 +709,13 @@ export function RouterProvider({
708709
[router, navigator, basename]
709710
);
710711

712+
let routerFuture = React.useMemo<RouterProps["future"]>(
713+
() => ({
714+
v7_relativeSplatPath: router.future.v7_relativeSplatPath,
715+
}),
716+
[router.future.v7_relativeSplatPath]
717+
);
718+
711719
// The fragment and {null} here are important! We need them to keep React 18's
712720
// useId happy when we are server-rendering since we may have a <script> here
713721
// containing the hydrated server-side staticContext (from StaticRouterProvider).
@@ -725,12 +733,10 @@ export function RouterProvider({
725733
location={state.location}
726734
navigationType={state.historyAction}
727735
navigator={navigator}
728-
future={{
729-
v7_relativeSplatPath: router.future.v7_relativeSplatPath,
730-
}}
736+
future={routerFuture}
731737
>
732738
{state.initialized || router.future.v7_partialHydration ? (
733-
<DataRoutes
739+
<MemoizedDataRoutes
734740
routes={router.routes}
735741
future={router.future}
736742
state={state}
@@ -748,6 +754,9 @@ export function RouterProvider({
748754
);
749755
}
750756

757+
// Memoize to avoid re-renders when updating `ViewTransitionContext`
758+
const MemoizedDataRoutes = React.memo(DataRoutes);
759+
751760
function DataRoutes({
752761
routes,
753762
future,

0 commit comments

Comments
 (0)