From a252428492ddaa490bee4a399cf36e5dc85886dc Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 17 Jul 2024 11:28:51 -0400 Subject: [PATCH] Reduce RouterProvider re-renders when using View Transitions (#11803) --- .changeset/fuzzy-worms-applaud.md | 5 ++ package.json | 2 +- .../__tests__/data-browser-router-test.tsx | 79 ++++++++++++++++++- packages/react-router-dom/index.tsx | 17 +++- 4 files changed, 96 insertions(+), 7 deletions(-) create mode 100644 .changeset/fuzzy-worms-applaud.md diff --git a/.changeset/fuzzy-worms-applaud.md b/.changeset/fuzzy-worms-applaud.md new file mode 100644 index 0000000000..2bc32ba093 --- /dev/null +++ b/.changeset/fuzzy-worms-applaud.md @@ -0,0 +1,5 @@ +--- +"react-router-dom": patch +--- + +Memoize some `RouterProvider` internals to reduce uneccesary re-renders diff --git a/package.json b/package.json index 34f174fa12..ba030b5f03 100644 --- a/package.json +++ b/package.json @@ -114,7 +114,7 @@ "none": "17.3 kB" }, "packages/react-router-dom/dist/react-router-dom.production.min.js": { - "none": "17.2 kB" + "none": "17.3 kB" }, "packages/react-router-dom/dist/umd/react-router-dom.production.min.js": { "none": "23.6 kB" diff --git a/packages/react-router-dom/__tests__/data-browser-router-test.tsx b/packages/react-router-dom/__tests__/data-browser-router-test.tsx index 0ae3c3824f..6881715c00 100644 --- a/packages/react-router-dom/__tests__/data-browser-router-test.tsx +++ b/packages/react-router-dom/__tests__/data-browser-router-test.tsx @@ -1,4 +1,4 @@ -import type { ErrorResponse, Fetcher } from "@remix-run/router"; +import type { ErrorResponse, Fetcher, RouterState } from "@remix-run/router"; import "@testing-library/jest-dom"; import { act, @@ -37,7 +37,7 @@ import { } from "react-router-dom"; import getHtml from "../../react-router/__tests__/utils/getHtml"; -import { createDeferred } from "../../router/__tests__/utils/utils"; +import { createDeferred, tick } from "../../router/__tests__/utils/utils"; testDomRouter("", createBrowserRouter, (url) => getWindowImpl(url, false) @@ -7465,6 +7465,81 @@ function testDomRouter( await waitFor(() => screen.getByText("D")); expect(spy).toHaveBeenCalledTimes(2); }); + + it("Does not cause extra re-renders due to ViewTransitionContext updates", async () => { + let testWindow = getWindow("/"); + testWindow.document.startViewTransition = (cb) => { + cb(); + return { + ready: Promise.resolve(), + finished: Promise.resolve(), + updateCallbackDone: Promise.resolve(), + skipTransition: () => {}, + }; + }; + + let renders: RouterState[] = []; + let router = createTestRouter( + [ + { + path: "/", + Component() { + return ( + <> + + /page + + + + ); + }, + children: [ + { + index: true, + async loader() { + await tick(); + return "INDEX"; + }, + Component() { + renders.push(useLocation(), useNavigation()); + return

{useLoaderData()}

; + }, + }, + { + path: "page", + async loader() { + await tick(); + return "PAGE"; + }, + Component() { + renders.push(useLocation(), useNavigation()); + return

{useLoaderData()}

; + }, + }, + ], + }, + ], + { window: testWindow } + ); + render(); + await waitFor(() => screen.getByText("INDEX")); + + renders = []; + fireEvent.click(screen.getByText("/page")); + await waitFor(() => screen.getByText("PAGE")); + + expect(renders).toMatchObject([ + // Re-render of current location with navigation.state = "loading" + { pathname: "/" }, + { + state: "loading", + location: { pathname: "/page" }, + }, + // Render of new location with navigation.state = "idle" + { pathname: "/page" }, + { state: "idle" }, + ]); + }); }); }); } diff --git a/packages/react-router-dom/index.tsx b/packages/react-router-dom/index.tsx index 77e6727e9e..22f903efd7 100644 --- a/packages/react-router-dom/index.tsx +++ b/packages/react-router-dom/index.tsx @@ -13,6 +13,7 @@ import type { Navigator, RelativeRoutingType, RouteObject, + RouterProps, RouterProviderProps, To, unstable_PatchRoutesOnMissFunction, @@ -708,6 +709,13 @@ export function RouterProvider({ [router, navigator, basename] ); + let routerFuture = React.useMemo( + () => ({ + v7_relativeSplatPath: router.future.v7_relativeSplatPath, + }), + [router.future.v7_relativeSplatPath] + ); + // 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