From 5b02e7d33d9251c74a96841979f4079694d19e6e Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 25 Jun 2024 11:38:49 -0400 Subject: [PATCH] Remove future.v7_partialHydration flag (#11725) --- .changeset/tasty-penguins-live.md | 11 + .../createRoutesFromChildren-test.tsx | 20 + .../__tests__/data-memory-router-test.tsx | 38 +- .../dom/data-browser-router-test.tsx | 36 +- .../__tests__/dom/partial-hydration-test.tsx | 276 ++------ .../__tests__/dom/ssr/meta-test.tsx | 1 + .../react-router/__tests__/dom/stub-test.tsx | 2 + .../__tests__/router/route-fallback-test.ts | 642 ++++++------------ .../__tests__/router/router-test.ts | 25 +- packages/react-router/lib/components.tsx | 2 + packages/react-router/lib/dom/lib.tsx | 26 +- packages/react-router/lib/dom/server.tsx | 6 +- packages/react-router/lib/dom/ssr/browser.tsx | 3 +- .../lib/dom/ssr/routes-test-stub.tsx | 1 + packages/react-router/lib/dom/ssr/server.tsx | 6 +- packages/react-router/lib/hooks.tsx | 2 +- packages/react-router/lib/router/router.ts | 25 +- 17 files changed, 368 insertions(+), 754 deletions(-) create mode 100644 .changeset/tasty-penguins-live.md diff --git a/.changeset/tasty-penguins-live.md b/.changeset/tasty-penguins-live.md new file mode 100644 index 0000000000..4ffae59f61 --- /dev/null +++ b/.changeset/tasty-penguins-live.md @@ -0,0 +1,11 @@ +--- +"react-router-dom": major +"react-router": major +--- + +- Remove the `future.v7_partialHydration` flag + - This also removes the `` prop + - To migrate, move the `fallbackElement` to a `hydrateFallbackElement`/`HydrateFallback` on your root route + - Also worth nothing there is a related breaking changer with this future flag: + - Without `future.v7_partialHydration` (when using `fallbackElement`), `state.navigation` was populated during the initial load + - With `future.v7_partialHydration`, `state.navigation` remains in an `"idle"` state during the initial load diff --git a/packages/react-router/__tests__/createRoutesFromChildren-test.tsx b/packages/react-router/__tests__/createRoutesFromChildren-test.tsx index 2d18fd2c7a..b3de9ad931 100644 --- a/packages/react-router/__tests__/createRoutesFromChildren-test.tsx +++ b/packages/react-router/__tests__/createRoutesFromChildren-test.tsx @@ -19,12 +19,14 @@ describe("creating routes from JSX", () => { { "Component": undefined, "ErrorBoundary": undefined, + "HydrateFallback": undefined, "action": undefined, "caseSensitive": undefined, "children": [ { "Component": undefined, "ErrorBoundary": undefined, + "HydrateFallback": undefined, "action": undefined, "caseSensitive": undefined, "element":

@@ -33,6 +35,7 @@ describe("creating routes from JSX", () => { "errorElement": undefined, "handle": undefined, "hasErrorBoundary": false, + "hydrateFallbackElement": undefined, "id": "0-0", "index": undefined, "lazy": undefined, @@ -43,6 +46,7 @@ describe("creating routes from JSX", () => { { "Component": undefined, "ErrorBoundary": undefined, + "HydrateFallback": undefined, "action": undefined, "caseSensitive": undefined, "element":

@@ -51,6 +55,7 @@ describe("creating routes from JSX", () => { "errorElement": undefined, "handle": undefined, "hasErrorBoundary": false, + "hydrateFallbackElement": undefined, "id": "0-1", "index": undefined, "lazy": undefined, @@ -61,12 +66,14 @@ describe("creating routes from JSX", () => { { "Component": undefined, "ErrorBoundary": undefined, + "HydrateFallback": undefined, "action": undefined, "caseSensitive": undefined, "children": [ { "Component": undefined, "ErrorBoundary": undefined, + "HydrateFallback": undefined, "action": undefined, "caseSensitive": undefined, "element":

@@ -75,6 +82,7 @@ describe("creating routes from JSX", () => { "errorElement": undefined, "handle": undefined, "hasErrorBoundary": false, + "hydrateFallbackElement": undefined, "id": "0-2-0", "index": true, "lazy": undefined, @@ -85,6 +93,7 @@ describe("creating routes from JSX", () => { { "Component": undefined, "ErrorBoundary": undefined, + "HydrateFallback": undefined, "action": undefined, "caseSensitive": undefined, "element":

@@ -93,6 +102,7 @@ describe("creating routes from JSX", () => { "errorElement": undefined, "handle": undefined, "hasErrorBoundary": false, + "hydrateFallbackElement": undefined, "id": "0-2-1", "index": undefined, "lazy": undefined, @@ -105,6 +115,7 @@ describe("creating routes from JSX", () => { "errorElement": undefined, "handle": undefined, "hasErrorBoundary": false, + "hydrateFallbackElement": undefined, "id": "0-2", "index": undefined, "lazy": undefined, @@ -117,6 +128,7 @@ describe("creating routes from JSX", () => { "errorElement": undefined, "handle": undefined, "hasErrorBoundary": false, + "hydrateFallbackElement": undefined, "id": "0", "index": undefined, "lazy": undefined, @@ -153,12 +165,14 @@ describe("creating routes from JSX", () => { { "Component": undefined, "ErrorBoundary": undefined, + "HydrateFallback": undefined, "action": undefined, "caseSensitive": undefined, "children": [ { "Component": undefined, "ErrorBoundary": undefined, + "HydrateFallback": undefined, "action": undefined, "caseSensitive": undefined, "element":

@@ -167,6 +181,7 @@ describe("creating routes from JSX", () => { "errorElement": undefined, "handle": undefined, "hasErrorBoundary": false, + "hydrateFallbackElement": undefined, "id": "0-0", "index": undefined, "lazy": undefined, @@ -177,12 +192,14 @@ describe("creating routes from JSX", () => { { "Component": undefined, "ErrorBoundary": undefined, + "HydrateFallback": undefined, "action": undefined, "caseSensitive": undefined, "children": [ { "Component": undefined, "ErrorBoundary": undefined, + "HydrateFallback": undefined, "action": [Function], "caseSensitive": undefined, "element":

@@ -191,6 +208,7 @@ describe("creating routes from JSX", () => { "errorElement": undefined, "handle": undefined, "hasErrorBoundary": false, + "hydrateFallbackElement": undefined, "id": "0-1-0", "index": true, "lazy": undefined, @@ -203,6 +221,7 @@ describe("creating routes from JSX", () => { "errorElement": undefined, "handle": undefined, "hasErrorBoundary": false, + "hydrateFallbackElement": undefined, "id": "0-1", "index": undefined, "lazy": undefined, @@ -217,6 +236,7 @@ describe("creating routes from JSX", () => {

, "handle": undefined, "hasErrorBoundary": true, + "hydrateFallbackElement": undefined, "id": "0", "index": undefined, "lazy": undefined, diff --git a/packages/react-router/__tests__/data-memory-router-test.tsx b/packages/react-router/__tests__/data-memory-router-test.tsx index fed0bbc1e8..adf92675a2 100644 --- a/packages/react-router/__tests__/data-memory-router-test.tsx +++ b/packages/react-router/__tests__/data-memory-router-test.tsx @@ -313,11 +313,15 @@ describe("createMemoryRouter", () => { `); }); - it("renders fallbackElement while first data fetch happens", async () => { + it("renders hydrateFallbackElement while first data fetch happens", async () => { let fooDefer = createDeferred(); let router = createMemoryRouter( createRoutesFromElements( - }> + } + hydrateFallbackElement={} + > fooDefer.promise} element={} /> } /> @@ -326,9 +330,7 @@ describe("createMemoryRouter", () => { initialEntries: ["/foo"], } ); - let { container } = render( - } /> - ); + let { container } = render(); function FallbackElement() { return

Loading...

; @@ -363,7 +365,7 @@ describe("createMemoryRouter", () => { `); }); - it("renders a null fallbackElement if none is provided", async () => { + it("renders a null fallback if none is provided", async () => { let fooDefer = createDeferred(); let router = createMemoryRouter( createRoutesFromElements( @@ -401,12 +403,16 @@ describe("createMemoryRouter", () => { `); }); - it("does not render fallbackElement if no data fetch is required", async () => { + it("does not render hydrateFallbackElement if no data fetch is required", async () => { let fooDefer = createDeferred(); let router = createMemoryRouter( createRoutesFromElements( - }> + } + hydrateFallbackElement={} + > fooDefer.promise} element={} /> } /> @@ -415,9 +421,7 @@ describe("createMemoryRouter", () => { initialEntries: ["/bar"], } ); - let { container } = render( - } /> - ); + let { container } = render(); function FallbackElement() { return

Loading...

; @@ -441,19 +445,21 @@ describe("createMemoryRouter", () => { `); }); - it("renders fallbackElement within router contexts", async () => { + it("renders hydrateFallbackElement within router contexts", async () => { let fooDefer = createDeferred(); let router = createMemoryRouter( createRoutesFromElements( - }> + } + hydrateFallbackElement={} + > fooDefer.promise} element={} /> ), { initialEntries: ["/foo"] } ); - let { container } = render( - } /> - ); + let { container } = render(); function FallbackElement() { let location = useLocation(); diff --git a/packages/react-router/__tests__/dom/data-browser-router-test.tsx b/packages/react-router/__tests__/dom/data-browser-router-test.tsx index 7dd2db831b..eb1a87d47b 100644 --- a/packages/react-router/__tests__/dom/data-browser-router-test.tsx +++ b/packages/react-router/__tests__/dom/data-browser-router-test.tsx @@ -371,11 +371,15 @@ function testDomRouter( `); }); - it("renders fallbackElement while first data fetch happens", async () => { + it("renders hydrateFallbackElement while first data fetch happens", async () => { let fooDefer = createDeferred(); let router = createTestRouter( createRoutesFromElements( - }> + } + hydrateFallbackElement={} + > fooDefer.promise} @@ -388,9 +392,7 @@ function testDomRouter( window: getWindow("/foo"), } ); - let { container } = render( - } /> - ); + let { container } = render(); function FallbackElement() { return

Loading...

; @@ -425,11 +427,15 @@ function testDomRouter( `); }); - it("renders fallbackElement while first data fetch and lazy route load happens", async () => { + it("renders hydrateFallbackElement while first data fetch and lazy route load happens", async () => { let fooDefer = createDeferred(); let router = createTestRouter( createRoutesFromElements( - }> + } + hydrateFallbackElement={} + > { @@ -446,9 +452,7 @@ function testDomRouter( window: getWindow("/foo"), } ); - let { container } = render( - } /> - ); + let { container } = render(); function FallbackElement() { return

Loading...

; @@ -526,11 +530,15 @@ function testDomRouter( `); }); - it("renders fallbackElement within router contexts", async () => { + it("renders hydrateFallbackElement within router contexts", async () => { let fooDefer = createDeferred(); let router = createTestRouter( createRoutesFromElements( - }> + } + hydrateFallbackElement={} + > fooDefer.promise} @@ -540,9 +548,7 @@ function testDomRouter( ), { window: getWindow("/foo") } ); - let { container } = render( - } /> - ); + let { container } = render(); function FallbackElement() { let location = useLocation(); diff --git a/packages/react-router/__tests__/dom/partial-hydration-test.tsx b/packages/react-router/__tests__/dom/partial-hydration-test.tsx index a18d1324b5..f98b401d1e 100644 --- a/packages/react-router/__tests__/dom/partial-hydration-test.tsx +++ b/packages/react-router/__tests__/dom/partial-hydration-test.tsx @@ -18,7 +18,7 @@ import { createDeferred, tick } from "../router/utils/utils"; let didAssertMissingHydrateFallback = false; -describe("v7_partialHydration", () => { +describe("Partial Hydration Behavior", () => { describe("createBrowserRouter", () => { testPartialHydration(createBrowserRouter, ReactRouterDom_RouterProvider); }); @@ -51,58 +51,6 @@ function testPartialHydration( consoleWarn.mockRestore(); }); - it("does not handle partial hydration by default", async () => { - let router = createTestRouter( - [ - { - id: "root", - path: "/", - loader: () => "ROOT", - Component() { - let data = useLoaderData() as string; - return ( - <> -

{`Home - ${data}`}

- - - ); - }, - children: [ - { - id: "index", - index: true, - loader: () => "INDEX", - HydrateFallback: () =>

Should not see me

, - Component() { - let data = useLoaderData() as string; - return

{`Index - ${data}`}

; - }, - }, - ], - }, - ], - { - hydrationData: { - loaderData: { - root: "HYDRATED ROOT", - }, - }, - } - ); - let { container } = render(); - - expect(getHtml(container)).toMatchInlineSnapshot(` - "
-

- Home - HYDRATED ROOT -

-

- Index - undefined -

-
" - `); - }); - it("supports partial hydration w/leaf fallback", async () => { let dfd = createDeferred(); let router = createTestRouter( @@ -140,9 +88,6 @@ function testPartialHydration( root: "HYDRATED ROOT", }, }, - future: { - v7_partialHydration: true, - }, } ); let { container } = render(); @@ -209,9 +154,6 @@ function testPartialHydration( root: "HYDRATED ROOT", }, }, - future: { - v7_partialHydration: true, - }, } ); let { container } = render(); @@ -274,9 +216,6 @@ function testPartialHydration( root: "HYDRATED ROOT", }, }, - future: { - v7_partialHydration: true, - }, } ); let { container } = render(); @@ -307,84 +246,6 @@ function testPartialHydration( `); }); - it("deprecates fallbackElement", async () => { - let dfd1 = createDeferred(); - let dfd2 = createDeferred(); - let router = createTestRouter( - [ - { - id: "root", - path: "/", - loader: () => dfd1.promise, - HydrateFallback: () =>

Root Loading...

, - Component() { - let data = useLoaderData() as string; - return ( - <> -

{`Home - ${data}`}

- - - ); - }, - children: [ - { - id: "index", - index: true, - loader: () => dfd2.promise, - Component() { - let data = useLoaderData() as string; - return

{`Index - ${data}`}

; - }, - }, - ], - }, - ], - { - hydrationData: { - loaderData: { - root: "HYDRATED ROOT", - }, - }, - future: { - v7_partialHydration: true, - }, - } - ); - let { container } = render( - fallbackElement...

} - /> - ); - - expect(getHtml(container)).toMatchInlineSnapshot(` - "
-

- Root Loading... -

-
" - `); - - expect(consoleWarn).toHaveBeenCalledWith( - "`` is deprecated when using " + - "`v7_partialHydration`, use a `HydrateFallback` component instead" - ); - - dfd1.resolve("ROOT DATA"); - dfd2.resolve("INDEX DATA"); - await waitFor(() => screen.getByText(/INDEX DATA/)); - expect(getHtml(container)).toMatchInlineSnapshot(` - "
-

- Home - HYDRATED ROOT -

-

- Index - INDEX DATA -

-
" - `); - }); - it("does not re-run loaders that don't have loader data due to errors", async () => { let spy = jest.fn(); let router = createTestRouter( @@ -429,9 +290,6 @@ function testPartialHydration( index: "INDEX ERROR", }, }, - future: { - v7_partialHydration: true, - }, } ); let { container } = render(); @@ -490,9 +348,6 @@ function testPartialHydration( index: "INDEX INITIAL", }, }, - future: { - v7_partialHydration: true, - }, } ); let { container } = render(); @@ -524,43 +379,36 @@ function testPartialHydration( it("supports partial hydration w/lazy initial routes (leaf fallback)", async () => { let dfd = createDeferred(); - let router = createTestRouter( - [ - { - path: "/", - Component() { - return ( - <> -

Root

- - - ); - }, - children: [ - { - id: "index", - index: true, - HydrateFallback: () =>

Index Loading...

, - async lazy() { - await tick(); - return { - loader: () => dfd.promise, - Component() { - let data = useLoaderData() as string; - return

{`Index - ${data}`}

; - }, - }; - }, - }, - ], - }, - ], + let router = createTestRouter([ { - future: { - v7_partialHydration: true, + path: "/", + Component() { + return ( + <> +

Root

+ + + ); }, - } - ); + children: [ + { + id: "index", + index: true, + HydrateFallback: () =>

Index Loading...

, + async lazy() { + await tick(); + return { + loader: () => dfd.promise, + Component() { + let data = useLoaderData() as string; + return

{`Index - ${data}`}

; + }, + }; + }, + }, + ], + }, + ]); let { container } = render(); expect(getHtml(container)).toMatchInlineSnapshot(` @@ -590,43 +438,36 @@ function testPartialHydration( it("supports partial hydration w/lazy initial routes (root fallback)", async () => { let dfd = createDeferred(); - let router = createTestRouter( - [ - { - path: "/", - Component() { - return ( - <> -

Root

- - - ); - }, - HydrateFallback: () =>

Loading...

, - children: [ - { - id: "index", - index: true, - async lazy() { - await tick(); - return { - loader: () => dfd.promise, - Component() { - let data = useLoaderData() as string; - return

{`Index - ${data}`}

; - }, - }; - }, - }, - ], - }, - ], + let router = createTestRouter([ { - future: { - v7_partialHydration: true, + path: "/", + Component() { + return ( + <> +

Root

+ + + ); }, - } - ); + HydrateFallback: () =>

Loading...

, + children: [ + { + id: "index", + index: true, + async lazy() { + await tick(); + return { + loader: () => dfd.promise, + Component() { + let data = useLoaderData() as string; + return

{`Index - ${data}`}

; + }, + }; + }, + }, + ], + }, + ]); let { container } = render(); expect(getHtml(container)).toMatchInlineSnapshot(` @@ -699,9 +540,6 @@ function testPartialHydration( index: "INDEX ERROR", }, }, - future: { - v7_partialHydration: true, - }, } ); let { container } = render(); diff --git a/packages/react-router/__tests__/dom/ssr/meta-test.tsx b/packages/react-router/__tests__/dom/ssr/meta-test.tsx index 018dd8a83f..03f43b32ff 100644 --- a/packages/react-router/__tests__/dom/ssr/meta-test.tsx +++ b/packages/react-router/__tests__/dom/ssr/meta-test.tsx @@ -283,6 +283,7 @@ describe("meta", () => { }, ], }), + HydrateFallback: () => null, Component() { let [count, setCount] = React.useState(0); return ( diff --git a/packages/react-router/__tests__/dom/stub-test.tsx b/packages/react-router/__tests__/dom/stub-test.tsx index b113778b98..f7862ef4d2 100644 --- a/packages/react-router/__tests__/dom/stub-test.tsx +++ b/packages/react-router/__tests__/dom/stub-test.tsx @@ -56,6 +56,7 @@ test("loaders work", async () => { let RoutesStub = createRoutesStub([ { path: "/", + HydrateFallback: () => null, Component() { let data = useLoaderData(); return
Message: {data.message}
; @@ -148,6 +149,7 @@ test("can pass context values", async () => { [ { path: "/", + HydrateFallback: () => null, Component() { let data = useLoaderData() as { context: string }; return ( diff --git a/packages/react-router/__tests__/router/route-fallback-test.ts b/packages/react-router/__tests__/router/route-fallback-test.ts index ebe98deb38..05ac55824b 100644 --- a/packages/react-router/__tests__/router/route-fallback-test.ts +++ b/packages/react-router/__tests__/router/route-fallback-test.ts @@ -44,480 +44,257 @@ afterEach(() => { router.dispose(); }); -describe("future.v7_partialHydration", () => { - describe("when set to false (default behavior)", () => { - it("starts with initialized=true when no loaders exist without hydrationData", async () => { - router = createRouter({ - routes: [ - { - id: "root", - path: "/", - }, - ], - history: createMemoryHistory(), - }); - expect(router.state).toMatchObject({ - historyAction: "POP", - location: { pathname: "/" }, - matches: [{ pathname: "/", route: { id: "root" } }], - initialized: true, - navigation: { state: "idle" }, - }); - }); - - it("starts with initialized=false when loaders exist without hydrationData", async () => { - router = createRouter({ - routes: [ - { - id: "root", - path: "/", - loader: () => Promise.resolve("LOADER DATA"), - }, - ], - history: createMemoryHistory(), - }); - expect(router.state).toMatchObject({ - historyAction: "POP", - location: { pathname: "/" }, - loaderData: {}, - matches: [{ pathname: "/", route: { id: "root" } }], - initialized: false, - navigation: { state: "idle" }, - }); - - router.initialize(); - await tick(); - expect(router.state).toMatchObject({ - historyAction: "POP", - location: { pathname: "/" }, - loaderData: { root: "LOADER DATA" }, - matches: [{ pathname: "/", route: { id: "root" } }], - initialized: true, - navigation: { state: "idle" }, - }); - }); - - it("starts with initialized=true when loaders exist with full hydrationData", async () => { - let spy = jest.fn(); - router = createRouter({ - routes: [ - { - id: "root", - path: "/", - loader: spy, - }, - ], - history: createMemoryHistory(), - hydrationData: { - loaderData: { root: "LOADER DATA" }, +describe("route HydrateFallback", () => { + it("starts with initialized=false, runs unhydrated loaders with partial hydrationData", async () => { + let spy = jest.fn(); + let shouldRevalidateSpy = jest.fn((args) => args.defaultShouldRevalidate); + let dfd = createDeferred(); + router = createRouter({ + routes: [ + { + id: "root", + path: "/", + loader: spy, + shouldRevalidate: shouldRevalidateSpy, + children: [ + { + id: "index", + index: true, + loader: () => dfd.promise, + }, + ], }, - }); - expect(router.state).toMatchObject({ - historyAction: "POP", - location: { pathname: "/" }, - loaderData: { root: "LOADER DATA" }, - matches: [{ pathname: "/", route: { id: "root" } }], - initialized: true, - navigation: { state: "idle" }, - }); - expect(spy).not.toHaveBeenCalled(); - }); - - it("starts with initialized=true when loaders exist with full hydrationData (+actions/errors)", async () => { - let spy = jest.fn(); - router = createRouter({ - routes: [ - { - id: "root", - path: "/", - hasErrorBoundary: true, - loader: spy, - action: spy, - }, - ], - history: createMemoryHistory(), - hydrationData: { - loaderData: { root: "LOADER DATA" }, - actionData: { root: "ACTION DATA" }, - errors: { root: new Error("lol") }, + ], + history: createMemoryHistory(), + hydrationData: { + loaderData: { + root: "LOADER DATA", + // No loaderData provided for index route }, - }); - expect(router.state).toMatchObject({ - historyAction: "POP", - location: { pathname: "/" }, - loaderData: { root: "LOADER DATA" }, - actionData: { root: "ACTION DATA" }, - errors: { root: new Error("lol") }, - matches: [{ pathname: "/", route: { id: "root" } }], - initialized: true, - navigation: { state: "idle" }, - }); - expect(spy).not.toHaveBeenCalled(); + }, }); - // This is needed because we can't detect valid "I have a loader" routes - // in Remix since all routes have a loader to fetch JS bundles but may not - // actually provide any loaderData - it("starts with initialized=true when loaders exist with partial hydration data", async () => { - let parentSpy = jest.fn(); - let childSpy = jest.fn(); - let router = createRouter({ - history: createMemoryHistory({ initialEntries: ["/child"] }), - routes: [ - { - path: "/", - loader: parentSpy, - children: [ - { - path: "child", - loader: childSpy, - }, - ], - }, - ], - hydrationData: { - loaderData: { - "0": "PARENT DATA", - }, - }, - }); - router.initialize(); + let subscriberSpy = jest.fn(); + router.subscribe(subscriberSpy); - expect(parentSpy.mock.calls.length).toBe(0); - expect(childSpy.mock.calls.length).toBe(0); - expect(router.state).toMatchObject({ - historyAction: "POP", - location: expect.objectContaining({ pathname: "/child" }), - matches: [{ route: { path: "/" } }, { route: { path: "child" } }], - initialized: true, - navigation: IDLE_NAVIGATION, - }); - expect(router.state.loaderData).toEqual({ - "0": "PARENT DATA", - }); - - router.dispose(); + // Start with initialized:false + expect(router.state).toMatchObject({ + historyAction: "POP", + location: { pathname: "/" }, + loaderData: { root: "LOADER DATA" }, + initialized: false, + navigation: { state: "idle" }, }); - it("does not kick off initial data load if errors exist", async () => { - let consoleWarnSpy = jest - .spyOn(console, "warn") - .mockImplementation(() => {}); - let parentDfd = createDeferred(); - let parentSpy = jest.fn(() => parentDfd.promise); - let childDfd = createDeferred(); - let childSpy = jest.fn(() => childDfd.promise); - let router = createRouter({ - history: createMemoryHistory({ initialEntries: ["/child"] }), - routes: [ - { - path: "/", - loader: parentSpy, - children: [ - { - path: "child", - loader: childSpy, - }, - ], - }, - ], - hydrationData: { - errors: { - "0": "PARENT ERROR", - }, - loaderData: { - "0-0": "CHILD_DATA", - }, - }, - }); - router.initialize(); + // Initialize/kick off data loads due to partial hydrationData + router.initialize(); + await dfd.resolve("INDEX DATA"); + expect(router.state).toMatchObject({ + historyAction: "POP", + location: { pathname: "/" }, + loaderData: { root: "LOADER DATA", index: "INDEX DATA" }, + initialized: true, + navigation: { state: "idle" }, + }); - expect(consoleWarnSpy).not.toHaveBeenCalled(); - expect(parentSpy).not.toHaveBeenCalled(); - expect(childSpy).not.toHaveBeenCalled(); - expect(router.state).toMatchObject({ - historyAction: "POP", - location: expect.objectContaining({ pathname: "/child" }), - matches: [{ route: { path: "/" } }, { route: { path: "child" } }], - initialized: true, - navigation: IDLE_NAVIGATION, - errors: { - "0": "PARENT ERROR", - }, - loaderData: { - "0-0": "CHILD_DATA", - }, - }); + // Root was not re-called + expect(shouldRevalidateSpy).not.toHaveBeenCalled(); + expect(spy).not.toHaveBeenCalled(); - router.dispose(); - consoleWarnSpy.mockReset(); + // Ensure we don't go into a navigating state during initial calls of + // the loaders + expect(subscriberSpy).toHaveBeenCalledTimes(1); + expect(subscriberSpy.mock.calls[0][0]).toMatchObject({ + loaderData: { + index: "INDEX DATA", + root: "LOADER DATA", + }, + navigation: IDLE_NAVIGATION, }); }); - describe("when set to true", () => { - it("starts with initialized=false, runs unhydrated loaders with partial hydrationData", async () => { - let spy = jest.fn(); - let shouldRevalidateSpy = jest.fn((args) => args.defaultShouldRevalidate); - let dfd = createDeferred(); - router = createRouter({ - routes: [ - { - id: "root", - path: "/", - loader: spy, - shouldRevalidate: shouldRevalidateSpy, - children: [ - { - id: "index", - index: true, - loader: () => dfd.promise, - }, - ], - }, - ], - history: createMemoryHistory(), - hydrationData: { - loaderData: { - root: "LOADER DATA", - // No loaderData provided for index route - }, - }, - future: { - v7_partialHydration: true, + it("starts with initialized=false, runs hydrated loaders when loader.hydrate=true", async () => { + let spy = jest.fn(); + let shouldRevalidateSpy = jest.fn((args) => args.defaultShouldRevalidate); + let dfd = createDeferred(); + let indexLoader: LoaderFunction = () => dfd.promise; + indexLoader.hydrate = true; + router = createRouter({ + routes: [ + { + id: "root", + path: "/", + loader: spy, + shouldRevalidate: shouldRevalidateSpy, + children: [ + { + id: "index", + index: true, + loader: indexLoader, + }, + ], }, - }); - - let subscriberSpy = jest.fn(); - router.subscribe(subscriberSpy); - - // Start with initialized:false - expect(router.state).toMatchObject({ - historyAction: "POP", - location: { pathname: "/" }, - loaderData: { root: "LOADER DATA" }, - initialized: false, - navigation: { state: "idle" }, - }); - - // Initialize/kick off data loads due to partial hydrationData - router.initialize(); - await dfd.resolve("INDEX DATA"); - expect(router.state).toMatchObject({ - historyAction: "POP", - location: { pathname: "/" }, - loaderData: { root: "LOADER DATA", index: "INDEX DATA" }, - initialized: true, - navigation: { state: "idle" }, - }); - - // Root was not re-called - expect(shouldRevalidateSpy).not.toHaveBeenCalled(); - expect(spy).not.toHaveBeenCalled(); - - // Ensure we don't go into a navigating state during initial calls of - // the loaders - expect(subscriberSpy).toHaveBeenCalledTimes(1); - expect(subscriberSpy.mock.calls[0][0]).toMatchObject({ + ], + history: createMemoryHistory(), + hydrationData: { loaderData: { - index: "INDEX DATA", root: "LOADER DATA", + index: "INDEX INITIAL", }, - navigation: IDLE_NAVIGATION, - }); + }, }); - it("starts with initialized=false, runs hydrated loaders when loader.hydrate=true", async () => { - let spy = jest.fn(); - let shouldRevalidateSpy = jest.fn((args) => args.defaultShouldRevalidate); - let dfd = createDeferred(); - let indexLoader: LoaderFunction = () => dfd.promise; - indexLoader.hydrate = true; - router = createRouter({ - routes: [ - { - id: "root", - path: "/", - loader: spy, - shouldRevalidate: shouldRevalidateSpy, - children: [ - { - id: "index", - index: true, - loader: indexLoader, - }, - ], - }, - ], - history: createMemoryHistory(), - hydrationData: { - loaderData: { - root: "LOADER DATA", - index: "INDEX INITIAL", - }, - }, - future: { - v7_partialHydration: true, - }, - }); - - let subscriberSpy = jest.fn(); - router.subscribe(subscriberSpy); + let subscriberSpy = jest.fn(); + router.subscribe(subscriberSpy); - // Start with initialized:false - expect(router.state).toMatchObject({ - historyAction: "POP", - location: { pathname: "/" }, - loaderData: { - root: "LOADER DATA", - index: "INDEX INITIAL", - }, - initialized: false, - navigation: { state: "idle" }, - }); + // Start with initialized:false + expect(router.state).toMatchObject({ + historyAction: "POP", + location: { pathname: "/" }, + loaderData: { + root: "LOADER DATA", + index: "INDEX INITIAL", + }, + initialized: false, + navigation: { state: "idle" }, + }); - // Initialize/kick off data loads due to partial hydrationData - router.initialize(); - await dfd.resolve("INDEX UPDATED"); - expect(router.state).toMatchObject({ - historyAction: "POP", - location: { pathname: "/" }, - loaderData: { - root: "LOADER DATA", - index: "INDEX UPDATED", - }, - initialized: true, - navigation: { state: "idle" }, - }); + // Initialize/kick off data loads due to partial hydrationData + router.initialize(); + await dfd.resolve("INDEX UPDATED"); + expect(router.state).toMatchObject({ + historyAction: "POP", + location: { pathname: "/" }, + loaderData: { + root: "LOADER DATA", + index: "INDEX UPDATED", + }, + initialized: true, + navigation: { state: "idle" }, + }); - // Root was not re-called - expect(shouldRevalidateSpy).not.toHaveBeenCalled(); - expect(spy).not.toHaveBeenCalled(); + // Root was not re-called + expect(shouldRevalidateSpy).not.toHaveBeenCalled(); + expect(spy).not.toHaveBeenCalled(); - // Ensure we don't go into a navigating state during initial calls of - // the loaders - expect(subscriberSpy).toHaveBeenCalledTimes(1); - expect(subscriberSpy.mock.calls[0][0]).toMatchObject({ - loaderData: { - index: "INDEX UPDATED", - root: "LOADER DATA", - }, - navigation: IDLE_NAVIGATION, - }); + // Ensure we don't go into a navigating state during initial calls of + // the loaders + expect(subscriberSpy).toHaveBeenCalledTimes(1); + expect(subscriberSpy.mock.calls[0][0]).toMatchObject({ + loaderData: { + index: "INDEX UPDATED", + root: "LOADER DATA", + }, + navigation: IDLE_NAVIGATION, }); + }); - it("does not kick off initial data load if errors exist (parent error)", async () => { - let consoleWarnSpy = jest - .spyOn(console, "warn") - .mockImplementation(() => {}); - let parentDfd = createDeferred(); - let parentSpy = jest.fn(() => parentDfd.promise); - let childDfd = createDeferred(); - let childSpy = jest.fn(() => childDfd.promise); - let router = createRouter({ - history: createMemoryHistory({ initialEntries: ["/child"] }), - routes: [ - { - path: "/", - loader: parentSpy, - children: [ - { - path: "child", - loader: childSpy, - }, - ], - }, - ], - future: { - v7_partialHydration: true, - }, - hydrationData: { - errors: { - "0": "PARENT ERROR", - }, - loaderData: { - "0-0": "CHILD_DATA", - }, + it("does not kick off initial data load if errors exist (parent error)", async () => { + let consoleWarnSpy = jest + .spyOn(console, "warn") + .mockImplementation(() => {}); + let parentDfd = createDeferred(); + let parentSpy = jest.fn(() => parentDfd.promise); + let childDfd = createDeferred(); + let childSpy = jest.fn(() => childDfd.promise); + let router = createRouter({ + history: createMemoryHistory({ initialEntries: ["/child"] }), + routes: [ + { + path: "/", + loader: parentSpy, + children: [ + { + path: "child", + loader: childSpy, + }, + ], }, - }); - router.initialize(); - - expect(consoleWarnSpy).not.toHaveBeenCalled(); - expect(parentSpy).not.toHaveBeenCalled(); - expect(childSpy).not.toHaveBeenCalled(); - expect(router.state).toMatchObject({ - historyAction: "POP", - location: expect.objectContaining({ pathname: "/child" }), - matches: [{ route: { path: "/" } }, { route: { path: "child" } }], - initialized: true, - navigation: IDLE_NAVIGATION, + ], + hydrationData: { errors: { "0": "PARENT ERROR", }, loaderData: { "0-0": "CHILD_DATA", }, - }); + }, + }); + router.initialize(); - router.dispose(); - consoleWarnSpy.mockReset(); + expect(consoleWarnSpy).not.toHaveBeenCalled(); + expect(parentSpy).not.toHaveBeenCalled(); + expect(childSpy).not.toHaveBeenCalled(); + expect(router.state).toMatchObject({ + historyAction: "POP", + location: expect.objectContaining({ pathname: "/child" }), + matches: [{ route: { path: "/" } }, { route: { path: "child" } }], + initialized: true, + navigation: IDLE_NAVIGATION, + errors: { + "0": "PARENT ERROR", + }, + loaderData: { + "0-0": "CHILD_DATA", + }, }); - it("does not kick off initial data load if errors exist (bubbled child error)", async () => { - let consoleWarnSpy = jest - .spyOn(console, "warn") - .mockImplementation(() => {}); - let parentDfd = createDeferred(); - let parentSpy = jest.fn(() => parentDfd.promise); - let childDfd = createDeferred(); - let childSpy = jest.fn(() => childDfd.promise); - let router = createRouter({ - history: createMemoryHistory({ initialEntries: ["/child"] }), - routes: [ - { - path: "/", - loader: parentSpy, - children: [ - { - path: "child", - loader: childSpy, - }, - ], - }, - ], - future: { - v7_partialHydration: true, - }, - hydrationData: { - errors: { - "0": "CHILD ERROR", - }, - loaderData: { - "0": "PARENT DATA", - }, - }, - }); - router.initialize(); + router.dispose(); + consoleWarnSpy.mockReset(); + }); - expect(consoleWarnSpy).not.toHaveBeenCalled(); - expect(parentSpy).not.toHaveBeenCalled(); - expect(childSpy).not.toHaveBeenCalled(); - expect(router.state).toMatchObject({ - historyAction: "POP", - location: expect.objectContaining({ pathname: "/child" }), - matches: [{ route: { path: "/" } }, { route: { path: "child" } }], - initialized: true, - navigation: IDLE_NAVIGATION, + it("does not kick off initial data load if errors exist (bubbled child error)", async () => { + let consoleWarnSpy = jest + .spyOn(console, "warn") + .mockImplementation(() => {}); + let parentDfd = createDeferred(); + let parentSpy = jest.fn(() => parentDfd.promise); + let childDfd = createDeferred(); + let childSpy = jest.fn(() => childDfd.promise); + let router = createRouter({ + history: createMemoryHistory({ initialEntries: ["/child"] }), + routes: [ + { + path: "/", + loader: parentSpy, + children: [ + { + path: "child", + loader: childSpy, + }, + ], + }, + ], + hydrationData: { errors: { "0": "CHILD ERROR", }, loaderData: { "0": "PARENT DATA", }, - }); + }, + }); + router.initialize(); - router.dispose(); - consoleWarnSpy.mockReset(); + expect(consoleWarnSpy).not.toHaveBeenCalled(); + expect(parentSpy).not.toHaveBeenCalled(); + expect(childSpy).not.toHaveBeenCalled(); + expect(router.state).toMatchObject({ + historyAction: "POP", + location: expect.objectContaining({ pathname: "/child" }), + matches: [{ route: { path: "/" } }, { route: { path: "child" } }], + initialized: true, + navigation: IDLE_NAVIGATION, + errors: { + "0": "CHILD ERROR", + }, + loaderData: { + "0": "PARENT DATA", + }, }); + + router.dispose(); + consoleWarnSpy.mockReset(); }); it("does not kick off initial data load for routes that don't have loaders", async () => { @@ -539,9 +316,6 @@ describe("future.v7_partialHydration", () => { ], }, ], - future: { - v7_partialHydration: true, - }, hydrationData: { loaderData: { "0": "PARENT DATA", diff --git a/packages/react-router/__tests__/router/router-test.ts b/packages/react-router/__tests__/router/router-test.ts index 49ba868309..eb733f9989 100644 --- a/packages/react-router/__tests__/router/router-test.ts +++ b/packages/react-router/__tests__/router/router-test.ts @@ -947,10 +947,7 @@ describe("a router", () => { historyAction: "POP", location: expect.objectContaining({ pathname: "/child" }), initialized: false, - navigation: { - state: "loading", - location: { pathname: "/child" }, - }, + navigation: IDLE_NAVIGATION, }); expect(router.state.loaderData).toEqual({}); @@ -959,10 +956,7 @@ describe("a router", () => { historyAction: "POP", location: expect.objectContaining({ pathname: "/child" }), initialized: false, - navigation: { - state: "loading", - location: { pathname: "/child" }, - }, + navigation: IDLE_NAVIGATION, }); expect(router.state.loaderData).toEqual({}); @@ -1016,10 +1010,7 @@ describe("a router", () => { historyAction: "POP", location: expect.objectContaining({ pathname: "/child" }), initialized: false, - navigation: { - state: "loading", - location: { pathname: "/child" }, - }, + navigation: IDLE_NAVIGATION, }); expect(router.state.loaderData).toEqual({}); @@ -1028,10 +1019,7 @@ describe("a router", () => { historyAction: "POP", location: expect.objectContaining({ pathname: "/child" }), initialized: false, - navigation: { - state: "loading", - location: { pathname: "/child" }, - }, + navigation: IDLE_NAVIGATION, }); expect(router.state.loaderData).toEqual({}); @@ -1118,10 +1106,7 @@ describe("a router", () => { historyAction: "POP", location: expect.objectContaining({ pathname: "/", hash: "#hash" }), initialized: false, - navigation: { - state: "loading", - location: { pathname: "/", hash: "#hash" }, - }, + navigation: IDLE_NAVIGATION, }); expect(router.state.loaderData).toEqual({}); diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx index 2528553e6b..e015b8d1cd 100644 --- a/packages/react-router/lib/components.tsx +++ b/packages/react-router/lib/components.tsx @@ -847,6 +847,8 @@ export function createRoutesFromChildren( path: element.props.path, loader: element.props.loader, action: element.props.action, + hydrateFallbackElement: element.props.hydrateFallbackElement, + HydrateFallback: element.props.HydrateFallback, errorElement: element.props.errorElement, ErrorBoundary: element.props.ErrorBoundary, hasErrorBoundary: diff --git a/packages/react-router/lib/dom/lib.tsx b/packages/react-router/lib/dom/lib.tsx index fbb27ed1b8..cb62d8bd83 100644 --- a/packages/react-router/lib/dom/lib.tsx +++ b/packages/react-router/lib/dom/lib.tsx @@ -307,7 +307,6 @@ class Deferred { } export interface RouterProviderProps { - fallbackElement?: React.ReactNode; router: RemixRouter; } @@ -315,7 +314,6 @@ export interface RouterProviderProps { * Given a Remix Router instance, render the appropriate UI */ export function RouterProvider({ - fallbackElement, router, }: RouterProviderProps): React.ReactElement { let [state, setStateImpl] = React.useState(router.state); @@ -485,16 +483,6 @@ export function RouterProvider({ } }, [vtContext.isTransitioning, interruption]); - React.useEffect(() => { - warning( - fallbackElement == null || !router.future.v7_partialHydration, - "`` is deprecated when using " + - "`v7_partialHydration`, use a `HydrateFallback` component instead" - ); - // Only log this once on initial mount - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - let navigator = React.useMemo((): Navigator => { return { createHref: router.createHref, @@ -544,15 +532,11 @@ export function RouterProvider({ navigationType={state.historyAction} navigator={navigator} > - {state.initialized || router.future.v7_partialHydration ? ( - - ) : ( - fallbackElement - )} + diff --git a/packages/react-router/lib/dom/server.tsx b/packages/react-router/lib/dom/server.tsx index 186ac0d82e..23239c8a2d 100644 --- a/packages/react-router/lib/dom/server.tsx +++ b/packages/react-router/lib/dom/server.tsx @@ -270,10 +270,7 @@ export function createStaticHandler( export function createStaticRouter( routes: RouteObject[], context: StaticHandlerContext, - opts: { - // Only accept future flags that impact the server render - future?: Partial>; - } = {} + opts: {} = {} ): RemixRouter { let manifest: RouteManifest = {}; let dataRoutes = convertRoutesToDataRoutes( @@ -304,7 +301,6 @@ export function createStaticRouter( get future() { return { v7_fetcherPersist: false, - v7_partialHydration: opts.future?.v7_partialHydration === true, v7_prependBasename: false, unstable_skipActionErrorRevalidation: false, }; diff --git a/packages/react-router/lib/dom/ssr/browser.tsx b/packages/react-router/lib/dom/ssr/browser.tsx index 15f35e67bc..14a294c186 100644 --- a/packages/react-router/lib/dom/ssr/browser.tsx +++ b/packages/react-router/lib/dom/ssr/browser.tsx @@ -163,7 +163,6 @@ function createHydratedRouter(): RemixRouter { basename: ssrInfo.context.basename, future: { v7_fetcherPersist: ssrInfo.context.future.v3_fetcherPersist, - v7_partialHydration: true, v7_prependBasename: true, // Single fetch enables this underlying behavior unstable_skipActionErrorRevalidation: true, @@ -254,7 +253,7 @@ export function HydratedRouter() { }} > - + {/* diff --git a/packages/react-router/lib/dom/ssr/routes-test-stub.tsx b/packages/react-router/lib/dom/ssr/routes-test-stub.tsx index 36a86ae85d..6843308926 100644 --- a/packages/react-router/lib/dom/ssr/routes-test-stub.tsx +++ b/packages/react-router/lib/dom/ssr/routes-test-stub.tsx @@ -165,6 +165,7 @@ function processRoutes( path: route.path, index: route.index, Component: route.Component, + HydrateFallback: route.HydrateFallback, ErrorBoundary: route.ErrorBoundary, action: action ? (args: ActionFunctionArgs) => action!({ ...args, context }) diff --git a/packages/react-router/lib/dom/ssr/server.tsx b/packages/react-router/lib/dom/ssr/server.tsx index 3ad66ecc5e..9e49f24317 100644 --- a/packages/react-router/lib/dom/ssr/server.tsx +++ b/packages/react-router/lib/dom/ssr/server.tsx @@ -65,11 +65,7 @@ export function ServerRouter({ } } - let router = createStaticRouter(routes, context.staticHandlerContext, { - future: { - v7_partialHydration: true, - }, - }); + let router = createStaticRouter(routes, context.staticHandlerContext); return ( <> diff --git a/packages/react-router/lib/hooks.tsx b/packages/react-router/lib/hooks.tsx index 7387d90c5f..1fdc491777 100644 --- a/packages/react-router/lib/hooks.tsx +++ b/packages/react-router/lib/hooks.tsx @@ -802,7 +802,7 @@ export function _renderMatches( // a given HydrateFallback while we load the rest of the hydration data let renderFallback = false; let fallbackIndex = -1; - if (dataRouterState && future && future.v7_partialHydration) { + if (dataRouterState) { for (let i = 0; i < renderedMatches.length; i++) { let match = renderedMatches[i]; // Track the deepest fallback up until the first route without data diff --git a/packages/react-router/lib/router/router.ts b/packages/react-router/lib/router/router.ts index 660f97bef7..58be37fcc9 100644 --- a/packages/react-router/lib/router/router.ts +++ b/packages/react-router/lib/router/router.ts @@ -365,7 +365,6 @@ export type HydrationState = Partial< */ export interface FutureConfig { v7_fetcherPersist: boolean; - v7_partialHydration: boolean; v7_prependBasename: boolean; unstable_skipActionErrorRevalidation: boolean; } @@ -842,7 +841,6 @@ export function createRouter(init: RouterInit): Router { // Config driven behavior flags let future: FutureConfig = { v7_fetcherPersist: false, - v7_partialHydration: false, v7_prependBasename: false, unstable_skipActionErrorRevalidation: false, ...init.future, @@ -891,8 +889,8 @@ export function createRouter(init: RouterInit): Router { } else if (!initialMatches.some((m) => m.route.loader)) { // If we've got no loaders to run, then we're good to go initialized = true; - } else if (future.v7_partialHydration) { - // If partial hydration is enabled, we're initialized so long as we were + } else { + // With "partial hydration", we're initialized so long as we were // provided with hydrationData for every route with a loader, and no loaders // were marked for explicit hydration let loaderData = init.hydrationData ? init.hydrationData.loaderData : null; @@ -925,10 +923,6 @@ export function createRouter(init: RouterInit): Router { } else { initialized = initialMatches.every(isRouteInitialized); } - } else { - // Without partial hydration - we're initialized if we were provided any - // hydrationData - which is expected to be complete - initialized = init.hydrationData != null; } let router: Router; @@ -1118,7 +1112,7 @@ export function createRouter(init: RouterInit): Router { // Kick off initial data load if needed. Use Pop to avoid modifying history // Note we don't do any handling of lazy here. For SPA's it'll get handled // in the normal navigation flow. For SSR it's expected that lazy modules are - // resolved prior to router creation since we can't go into a fallbackElement + // resolved prior to router creation since we can't go into a fallback // UI for SSR'd apps if (!state.initialized) { startNavigation(HistoryAction.Pop, state.location, { @@ -1866,11 +1860,10 @@ export function createRouter(init: RouterInit): Router { // state. If not, we need to switch to our loading state and load data, // preserving any new action data or existing action data (in the case of // a revalidation interrupting an actionReload) - // If we have partialHydration enabled, then don't update the state for the - // initial data load since it's not a "navigation" + // Also (with "partial hydration"), don't update the state for the initial + // data load since it's not a "navigation" let shouldUpdateNavigationState = - !isUninterruptedRevalidation && - (!future.v7_partialHydration || !initialHydration); + !isUninterruptedRevalidation && !initialHydration; // When fog of war is enabled, we enter our `loading` state earlier so we // can discover new routes during the `loading` state. We skip this if @@ -1934,7 +1927,7 @@ export function createRouter(init: RouterInit): Router { matches, activeSubmission, location, - future.v7_partialHydration && initialHydration === true, + initialHydration === true, future.unstable_skipActionErrorRevalidation, isRevalidationRequired, cancelledDeferredRoutes, @@ -2082,8 +2075,8 @@ export function createRouter(init: RouterInit): Router { }); }); - // During partial hydration, preserve SSR errors for routes that don't re-run - if (future.v7_partialHydration && initialHydration && state.errors) { + // With "partial hydration", preserve SSR errors for routes that don't re-run + if (initialHydration && state.errors) { Object.entries(state.errors) .filter(([id]) => !matchesToLoad.some((m) => m.route.id === id)) .forEach(([routeId, error]) => {