diff --git a/.changeset/weak-otters-dance.md b/.changeset/weak-otters-dance.md new file mode 100644 index 0000000000..fbe5d5ad2b --- /dev/null +++ b/.changeset/weak-otters-dance.md @@ -0,0 +1,6 @@ +--- +"react-router-dom": major +"react-router": major +--- + +Remove `future.v7_fetcherPersist` flag 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 3aada05c91..9d4c30991c 100644 --- a/packages/react-router/__tests__/dom/data-browser-router-test.tsx +++ b/packages/react-router/__tests__/dom/data-browser-router-test.tsx @@ -16,6 +16,7 @@ import { Link, Outlet, Route, + RouterProvider, createBrowserRouter, createHashRouter, createRoutesFromElements, @@ -35,9 +36,6 @@ import { useSubmit, } from "../../index"; -// TODO: Figure this out! -import { RouterProvider } from "../../lib/dom/lib"; - import getHtml from "../utils/getHtml"; import { createDeferred } from "../router/utils/utils"; @@ -4559,7 +4557,7 @@ function testDomRouter( expect(text).toEqual(body); }); - it("show all fetchers via useFetchers and cleans up fetchers on unmount", async () => { + it("show all active fetchers via useFetchers and cleans up fetchers on unmount", async () => { let navDfd = createDeferred(); let fetchDfd1 = createDeferred(); let fetchDfd2 = createDeferred(); @@ -4673,7 +4671,7 @@ function testDomRouter( id="output" >

- ["idle"] + []

1 @@ -4694,7 +4692,7 @@ function testDomRouter( id="output" >

- ["idle"] + []

1 @@ -4759,7 +4757,7 @@ function testDomRouter( id="output" >

- ["idle"] + []

2 @@ -5208,17 +5206,17 @@ function testDomRouter( dfd1.resolve("FETCH 1"); await waitFor(() => - screen.getByText("1, idle/FETCH 1, idle/undefined") + screen.getByText("0, idle/FETCH 1, idle/undefined") ); fireEvent.click(screen.getByText("Load 2")); await waitFor(() => - screen.getByText("2, idle/FETCH 1, loading/undefined") + screen.getByText("1, idle/FETCH 1, loading/undefined") ); dfd2.resolve("FETCH 2"); await waitFor(() => - screen.getByText("2, idle/FETCH 1, idle/FETCH 2") + screen.getByText("0, idle/FETCH 1, idle/FETCH 2") ); }); @@ -5270,7 +5268,7 @@ function testDomRouter( dfd1.resolve("FETCH 1"); await waitFor(() => - screen.getByText("1, idle/FETCH 1, idle/FETCH 1") + screen.getByText("0, idle/FETCH 1, idle/FETCH 1") ); fireEvent.click(screen.getByText("Load 2")); @@ -5280,7 +5278,7 @@ function testDomRouter( dfd2.resolve("FETCH 2"); await waitFor(() => - screen.getByText("1, idle/FETCH 2, idle/FETCH 2") + screen.getByText("0, idle/FETCH 2, idle/FETCH 2") ); }); @@ -5331,8 +5329,29 @@ function testDomRouter( let { container } = render(); // Start with idle fetcher 'a' - expect(getHtml(container)).toContain('{"Form":{},"state":"idle"}'); - expect(getHtml(container)).toContain("fetcherKey:a"); + expect(getHtml(container)).toMatchInlineSnapshot(` + "

+ +

+ fetcherKey:a +

+

+ Fetcher: + {"Form":{},"state":"idle"} +

+ +

+ Fetchers: +

+
+                []
+              
+
" + `); fireEvent.click(screen.getByText("Load Fetcher")); await waitFor( @@ -5340,778 +5359,761 @@ function testDomRouter( ); // Fetcher 'a' now has data - expect(getHtml(container)).toContain( - '{"Form":{},"state":"idle","data":"http://localhost/echo?fetcherKey=a"}' - ); - expect(getHtml(container)).toContain( - '[{"state":"idle","data":"http://localhost/echo?fetcherKey=a","key":"a"}]' - ); + expect(getHtml(container)).toMatchInlineSnapshot(` + "
+ +

+ fetcherKey:a +

+

+ Fetcher: + {"Form":{},"state":"idle","data":"http://localhost/echo?fetcherKey=a"} +

+ +

+ Fetchers: +

+
+                []
+              
+
" + `); fireEvent.click(screen.getByText("Change Key")); await waitFor(() => screen.getByText("fetcherKey:b")); // We should have a new uninitialized/idle fetcher 'b' - expect(getHtml(container)).toContain('{"Form":{},"state":"idle"'); - expect(getHtml(container)).toContain("[]"); + expect(getHtml(container)).toMatchInlineSnapshot(` + "
+ +

+ fetcherKey:b +

+

+ Fetcher: + {"Form":{},"state":"idle"} +

+ +

+ Fetchers: +

+
+                []
+              
+
" + `); }); it("exposes fetcher keys via useFetchers", async () => { + let dfd = createDeferred(); let router = createTestRouter( [ { path: "/", - loader: () => "FETCH", Component() { let fetcher1 = useFetcher(); let fetcher2 = useFetcher({ key: "my-key" }); let fetchers = useFetchers(); - React.useEffect(() => { - if (fetcher1.state === "idle" && !fetcher1.data) { - fetcher1.load("/"); - } - if (fetcher2.state === "idle" && !fetcher2.data) { - fetcher2.load("/"); - } - }, [fetcher1, fetcher2]); - return
{fetchers.map((f) => f.key).join(",")}
; + return ( + <> +
{fetchers.map((f) => f.key).join(",")}
+ + + ); }, }, + { + path: "/fetch", + loader: () => dfd.promise, + }, ], { window: getWindow("/") } ); let { container } = render(); expect(container.innerHTML).not.toMatch(/my-key/); + fireEvent.click(screen.getByText("Load fetchers")); await waitFor(() => - // React `useId()` results in either `:r2a:` or `:rp:` depending on + // React `useId()` results in either `:r28:` or `:rp:` depending on // `DataBrowserRouter`/`DataHashRouter` - expect(container.innerHTML).toMatch(/(:r2a:|:rp:),my-key/) + expect(container.innerHTML).toMatch(/(:r28:|:rp:),my-key/) ); }); }); describe("fetcher persistence", () => { - describe("default behavior", () => { - it("loading fetchers clean up on unmount by default", async () => { - let dfd = createDeferred(); - let loaderRequest: Request | null = null; - let router = createTestRouter( - [ - { - path: "/", - Component() { - let fetchers = useFetchers(); - return ( - <> -
{`Num fetchers: ${fetchers.length}`}
- Go to /page - - - ); - }, - children: [ - { - index: true, - Component() { - let fetcher = useFetcher(); - return ( - - ); - }, + it("loading fetchers persist until completion", async () => { + let dfd = createDeferred(); + let router = createTestRouter( + [ + { + path: "/", + Component() { + let fetchers = useFetchers(); + return ( + <> +
{`Num fetchers: ${fetchers.length}`}
+ Go to /page + + + ); + }, + children: [ + { + index: true, + Component() { + let fetcher = useFetcher(); + return ( + + ); }, - { - path: "page", - Component() { - return

Page

; - }, + }, + { + path: "page", + Component() { + return

Page

; }, - ], - }, - { - path: "/fetch", - loader: ({ request }) => { - loaderRequest = request; - return dfd.promise; }, - }, - ], - { window: getWindow("/") } - ); - let { container } = render(); + ], + }, + { + path: "/fetch", + loader: () => dfd.promise, + }, + ], + { window: getWindow("/") } + ); + let { container } = render(); - expect(getHtml(container)).toMatch("Num fetchers: 0"); + expect(getHtml(container)).toMatch("Num fetchers: 0"); - fireEvent.click(screen.getByText("Load (idle)")); - expect(getHtml(container)).toMatch("Num fetchers: 1"); - expect(getHtml(container)).toMatch("Load (loading)"); + fireEvent.click(screen.getByText("Load (idle)")); + expect(getHtml(container)).toMatch("Num fetchers: 1"); + expect(getHtml(container)).toMatch("Load (loading)"); - fireEvent.click(screen.getByText("Go to /page")); - await waitFor(() => screen.getByText("Page")); - expect(getHtml(container)).toMatch("Num fetchers: 0"); - expect(getHtml(container)).toMatch("Page"); + fireEvent.click(screen.getByText("Go to /page")); + await waitFor(() => screen.getByText("Page")); + expect(getHtml(container)).toMatch("Num fetchers: 1"); + expect(getHtml(container)).toMatch("Page"); - // Resolve after the navigation - no-op - expect((loaderRequest as unknown as Request)?.signal?.aborted).toBe( - true - ); - dfd.resolve("FETCH"); - await waitFor(() => screen.getByText("Num fetchers: 0")); - expect(getHtml(container)).toMatch("Page"); - }); - - it("submitting fetchers persist until completion", async () => { - let dfd = createDeferred(); - let router = createTestRouter( - [ - { - path: "/", - Component() { - let fetchers = useFetchers(); - return ( - <> -
{`Num fetchers: ${fetchers.length}`}
- Go to /page - - - ); - }, - children: [ - { - index: true, - Component() { - let fetcher = useFetcher(); - return ( - - ); - }, + // Resolve after the navigation - no-op + dfd.resolve("FETCH"); + await waitFor(() => screen.getByText("Num fetchers: 0")); + expect(getHtml(container)).toMatch("Page"); + }); + + it("submitting fetchers persist until completion", async () => { + let dfd = createDeferred(); + let router = createTestRouter( + [ + { + path: "/", + Component() { + let fetchers = useFetchers(); + return ( + <> +
{`Num fetchers: ${fetchers.length}`}
+ Go to /page + + + ); + }, + children: [ + { + index: true, + Component() { + let fetcher = useFetcher(); + return ( + + ); }, - { - path: "page", - Component() { - return

Page

; - }, + }, + { + path: "page", + Component() { + return

Page

; }, - ], - }, - { - path: "/fetch", - action: () => dfd.promise, - }, - ], - { window: getWindow("/") } - ); - let { container } = render(); + }, + ], + }, + { + path: "/fetch", + action: () => dfd.promise, + }, + ], + { window: getWindow("/") } + ); + let { container } = render(); - expect(getHtml(container)).toMatch("Num fetchers: 0"); + expect(getHtml(container)).toMatch("Num fetchers: 0"); - fireEvent.click(screen.getByText("Submit (idle)")); - expect(getHtml(container)).toMatch("Num fetchers: 1"); - expect(getHtml(container)).toMatch("Submit (submitting)"); + fireEvent.click(screen.getByText("Submit (idle)")); + expect(getHtml(container)).toMatch("Num fetchers: 1"); + expect(getHtml(container)).toMatch("Submit (submitting)"); - fireEvent.click(screen.getByText("Go to /page")); - await waitFor(() => screen.getByText("Page")); - expect(getHtml(container)).toMatch("Num fetchers: 0"); + fireEvent.click(screen.getByText("Go to /page")); + await waitFor(() => screen.getByText("Page")); + expect(getHtml(container)).toMatch("Num fetchers: 1"); - // Resolve after the navigation - trigger cleanup - dfd.resolve("FETCH"); - await waitFor(() => screen.getByText("Num fetchers: 0")); - }); + // Resolve after the navigation - trigger cleanup + dfd.resolve("FETCH"); + await waitFor(() => screen.getByText("Num fetchers: 0")); }); - describe("v7_fetcherPersist=true", () => { - it("loading fetchers persist until completion", async () => { - let dfd = createDeferred(); - let router = createTestRouter( - [ - { - path: "/", - Component() { - let fetchers = useFetchers(); - return ( - <> -
{`Num fetchers: ${fetchers.length}`}
- Go to /page - - - ); + it("submitting fetchers w/revalidations are cleaned up on completion", async () => { + let count = 0; + let dfd = createDeferred(); + let router = createTestRouter( + [ + { + path: "/", + Component() { + let fetchers = useFetchers(); + return ( + <> +
{`Num fetchers: ${fetchers.length}`}
+ Go to /page + + + ); + }, + children: [ + { + index: true, + Component() { + let fetcher = useFetcher(); + return ( + + ); + }, }, - children: [ - { - index: true, - Component() { - let fetcher = useFetcher(); - return ( - - ); - }, + { + path: "page", + Component() { + let data = useLoaderData() as { count: number }; + return

{`Page (${data.count})`}

; }, - { - path: "page", - Component() { - return

Page

; - }, + async loader() { + await new Promise((r) => setTimeout(r, 10)); + return { count: ++count }; }, - ], - }, - { - path: "/fetch", - loader: () => dfd.promise, - }, - ], - { window: getWindow("/"), future: { v7_fetcherPersist: true } } - ); - let { container } = render(); + }, + ], + }, + { + path: "/fetch", + action: () => dfd.promise, + }, + ], + { window: getWindow("/") } + ); + let { container } = render(); - expect(getHtml(container)).toMatch("Num fetchers: 0"); + expect(getHtml(container)).toMatch("Num fetchers: 0"); - fireEvent.click(screen.getByText("Load (idle)")); - expect(getHtml(container)).toMatch("Num fetchers: 1"); - expect(getHtml(container)).toMatch("Load (loading)"); + fireEvent.click(screen.getByText("Submit (idle)")); + expect(getHtml(container)).toMatch("Num fetchers: 1"); + expect(getHtml(container)).toMatch("Submit (submitting)"); - fireEvent.click(screen.getByText("Go to /page")); - await waitFor(() => screen.getByText("Page")); - expect(getHtml(container)).toMatch("Num fetchers: 1"); - expect(getHtml(container)).toMatch("Page"); + fireEvent.click(screen.getByText("Go to /page")); + await waitFor(() => screen.getByText("Page (1)")); + expect(getHtml(container)).toMatch("Num fetchers: 1"); - // Resolve after the navigation - no-op - dfd.resolve("FETCH"); - await waitFor(() => screen.getByText("Num fetchers: 0")); - expect(getHtml(container)).toMatch("Page"); - }); + // Resolve action after the navigation and trigger revalidation + dfd.resolve("FETCH"); + await waitFor(() => screen.getByText("Num fetchers: 0")); + expect(getHtml(container)).toMatch("Page (2)"); + }); - it("submitting fetchers persist until completion", async () => { - let dfd = createDeferred(); - let router = createTestRouter( - [ - { - path: "/", - Component() { - let fetchers = useFetchers(); - return ( - <> -
{`Num fetchers: ${fetchers.length}`}
- Go to /page - - - ); + it("submitting fetchers w/revalidations are cleaned up on completion (remounted)", async () => { + let count = 0; + let dfd = createDeferred(); + let router = createTestRouter( + [ + { + path: "/", + Component() { + let fetchers = useFetchers(); + return ( + <> +
{`Num fetchers: ${fetchers.length}`}
+ Go to /page + + + ); + }, + children: [ + { + index: true, + Component() { + let fetcher = useFetcher({ key: "me" }); + return ( + + ); + }, }, - children: [ - { - index: true, - Component() { - let fetcher = useFetcher(); - return ( - - ); - }, + { + path: "page", + Component() { + let fetcher = useFetcher({ key: "me" }); + let data = useLoaderData() as { count: number }; + return ( + <> +

{`Page (${data.count})`}

+

{fetcher.data}

+ + ); }, - { - path: "page", - Component() { - return

Page

; - }, + async loader() { + await new Promise((r) => setTimeout(r, 10)); + return { count: ++count }; }, - ], - }, - { - path: "/fetch", - action: () => dfd.promise, - }, - ], - { window: getWindow("/"), future: { v7_fetcherPersist: true } } - ); - let { container } = render(); + }, + ], + }, + { + path: "/fetch", + action: () => dfd.promise, + }, + ], + { window: getWindow("/") } + ); + let { container } = render(); - expect(getHtml(container)).toMatch("Num fetchers: 0"); + expect(getHtml(container)).toMatch("Num fetchers: 0"); - fireEvent.click(screen.getByText("Submit (idle)")); - expect(getHtml(container)).toMatch("Num fetchers: 1"); - expect(getHtml(container)).toMatch("Submit (submitting)"); + fireEvent.click(screen.getByText("Submit (idle)")); + expect(getHtml(container)).toMatch("Num fetchers: 1"); + expect(getHtml(container)).toMatch("Submit (submitting)"); - fireEvent.click(screen.getByText("Go to /page")); - await waitFor(() => screen.getByText("Page")); - expect(getHtml(container)).toMatch("Num fetchers: 1"); + fireEvent.click(screen.getByText("Go to /page")); + await waitFor(() => screen.getByText("Page (1)")); + expect(getHtml(container)).toMatch("Num fetchers: 1"); - // Resolve after the navigation - trigger cleanup - dfd.resolve("FETCH"); - await waitFor(() => screen.getByText("Num fetchers: 0")); - }); + // Resolve after the navigation and revalidation + dfd.resolve("FETCH"); + await waitFor(() => screen.getByText("Num fetchers: 0")); + expect(getHtml(container)).toMatch("Page (2)"); + expect(getHtml(container)).toMatch("FETCH"); + }); - it("submitting fetchers w/revalidations are cleaned up on completion", async () => { - let count = 0; - let dfd = createDeferred(); - let router = createTestRouter( - [ - { - path: "/", - Component() { - let fetchers = useFetchers(); - return ( - <> -
{`Num fetchers: ${fetchers.length}`}
- Go to /page - - - ); + it("submitting fetchers w/redirects are cleaned up on completion", async () => { + let dfd = createDeferred(); + let router = createTestRouter( + [ + { + path: "/", + Component() { + let fetchers = useFetchers(); + return ( + <> +
{`Num fetchers: ${fetchers.length}`}
+ Go to /page + + + ); + }, + children: [ + { + index: true, + Component() { + let fetcher = useFetcher(); + return ( + + ); + }, }, - children: [ - { - index: true, - Component() { - let fetcher = useFetcher(); - return ( - - ); - }, + { + path: "page", + Component() { + return

Page

; }, - { - path: "page", - Component() { - let data = useLoaderData() as { count: number }; - return

{`Page (${data.count})`}

; - }, - async loader() { - await new Promise((r) => setTimeout(r, 10)); - return { count: ++count }; - }, + }, + { + path: "redirect", + Component() { + return

Redirect

; }, - ], - }, - { - path: "/fetch", - action: () => dfd.promise, - }, - ], - { window: getWindow("/"), future: { v7_fetcherPersist: true } } - ); - let { container } = render(); + }, + ], + }, + { + path: "/fetch", + action: () => dfd.promise, + }, + ], + { window: getWindow("/") } + ); + let { container } = render(); - expect(getHtml(container)).toMatch("Num fetchers: 0"); + expect(getHtml(container)).toMatch("Num fetchers: 0"); - fireEvent.click(screen.getByText("Submit (idle)")); - expect(getHtml(container)).toMatch("Num fetchers: 1"); - expect(getHtml(container)).toMatch("Submit (submitting)"); + fireEvent.click(screen.getByText("Submit (idle)")); + expect(getHtml(container)).toMatch("Num fetchers: 1"); + expect(getHtml(container)).toMatch("Submit (submitting)"); - fireEvent.click(screen.getByText("Go to /page")); - await waitFor(() => screen.getByText("Page (1)")); - expect(getHtml(container)).toMatch("Num fetchers: 1"); + fireEvent.click(screen.getByText("Go to /page")); + await waitFor(() => screen.getByText("Page")); + expect(getHtml(container)).toMatch("Num fetchers: 1"); - // Resolve action after the navigation and trigger revalidation - dfd.resolve("FETCH"); - await waitFor(() => screen.getByText("Num fetchers: 0")); - expect(getHtml(container)).toMatch("Page (2)"); - }); + // Resolve after the navigation - trigger cleanup + // We don't process the redirect here since it was superseded by a + // navigation, but we assert that it gets cleaned up afterwards + dfd.resolve(redirect("/redirect")); + await waitFor(() => screen.getByText("Num fetchers: 0")); + expect(getHtml(container)).toMatch("Page"); + }); - it("submitting fetchers w/revalidations are cleaned up on completion (remounted)", async () => { - let count = 0; - let dfd = createDeferred(); - let router = createTestRouter( - [ - { - path: "/", - Component() { - let fetchers = useFetchers(); - return ( - <> -
{`Num fetchers: ${fetchers.length}`}
- Go to /page - - - ); - }, - children: [ - { - index: true, - Component() { - let fetcher = useFetcher({ key: "me" }); - return ( - - ); - }, + + ); }, - { - path: "page", - Component() { - let fetcher = useFetcher({ key: "me" }); - let data = useLoaderData() as { count: number }; - return ( - <> -

{`Page (${data.count})`}

-

{fetcher.data}

- - ); - }, - async loader() { - await new Promise((r) => setTimeout(r, 10)); - return { count: ++count }; - }, + }, + { + path: "page", + Component() { + return

Page

; }, - ], - }, - { - path: "/fetch", - action: () => dfd.promise, - }, - ], - { window: getWindow("/"), future: { v7_fetcherPersist: true } } - ); - let { container } = render(); + }, + ], + }, + { + path: "/fetch", + action: () => dfd.promise, + }, + ], + { window: getWindow("/") } + ); + let { container } = render(); - expect(getHtml(container)).toMatch("Num fetchers: 0"); + expect(getHtml(container)).toMatch("Num fetchers: 0"); - fireEvent.click(screen.getByText("Submit (idle)")); - expect(getHtml(container)).toMatch("Num fetchers: 1"); - expect(getHtml(container)).toMatch("Submit (submitting)"); + fireEvent.click(screen.getByText("Submit (idle)")); + expect(getHtml(container)).toMatch("Num fetchers: 1"); + expect(getHtml(container)).toMatch("Submit (submitting)"); - fireEvent.click(screen.getByText("Go to /page")); - await waitFor(() => screen.getByText("Page (1)")); - expect(getHtml(container)).toMatch("Num fetchers: 1"); + fireEvent.click(screen.getByText("Go to /page")); + await waitFor(() => screen.getByText("Page")); + expect(getHtml(container)).toMatch("Num fetchers: 1"); - // Resolve after the navigation and revalidation - dfd.resolve("FETCH"); - await waitFor(() => screen.getByText("Num fetchers: 0")); - expect(getHtml(container)).toMatch("Page (2)"); - expect(getHtml(container)).toMatch("FETCH"); - }); + // Resolve after the navigation and revalidation + dfd.resolve("FETCH"); + await waitFor(() => screen.getByText("Num fetchers: 0")); + }); - it("submitting fetchers w/redirects are cleaned up on completion", async () => { - let dfd = createDeferred(); - let router = createTestRouter( - [ - { - path: "/", - Component() { - let fetchers = useFetchers(); - return ( - <> -
{`Num fetchers: ${fetchers.length}`}
- Go to /page - - - ); - }, - children: [ - { - index: true, - Component() { - let fetcher = useFetcher(); - return ( - - ); - }, - }, - { - path: "page", - Component() { - return

Page

; - }, - }, - { - path: "redirect", - Component() { - return

Redirect

; - }, - }, - ], - }, - { - path: "/fetch", - action: () => dfd.promise, + it("unmounted fetcher.load errors should not bubble up to the UI", async () => { + let dfd = createDeferred(); + let router = createTestRouter( + [ + { + path: "/", + Component() { + let fetchers = useFetchers(); + return ( + <> +
{`Num fetchers: ${fetchers.length}`}
+ Go to /page + + + ); }, - ], - { window: getWindow("/"), future: { v7_fetcherPersist: true } } - ); - let { container } = render(); - - expect(getHtml(container)).toMatch("Num fetchers: 0"); - - fireEvent.click(screen.getByText("Submit (idle)")); - expect(getHtml(container)).toMatch("Num fetchers: 1"); - expect(getHtml(container)).toMatch("Submit (submitting)"); - - fireEvent.click(screen.getByText("Go to /page")); - await waitFor(() => screen.getByText("Page")); - expect(getHtml(container)).toMatch("Num fetchers: 1"); - - // Resolve after the navigation - trigger cleanup - // We don't process the redirect here since it was superseded by a - // navigation, but we assert that it gets cleaned up afterwards - dfd.resolve(redirect("/redirect")); - await waitFor(() => screen.getByText("Num fetchers: 0")); - expect(getHtml(container)).toMatch("Page"); - }); - - it("submitting fetcher.Form persist until completion", async () => { - let dfd = createDeferred(); - let router = createTestRouter( - [ - { - path: "/", - Component() { - let fetchers = useFetchers(); - return ( - <> -
{`Num fetchers: ${fetchers.length}`}
- Go to /page - - - ); - }, - children: [ - { - index: true, - Component() { - let fetcher = useFetcher(); - return ( - - - - ); - }, + children: [ + { + index: true, + Component() { + let fetcher = useFetcher(); + return ( + + ); }, - { - path: "page", - Component() { - return

Page

; - }, + }, + { + path: "page", + Component() { + return

Page

; }, - ], - }, - { - path: "/fetch", - action: () => dfd.promise, - }, - ], - { window: getWindow("/"), future: { v7_fetcherPersist: true } } - ); - let { container } = render(); + }, + ], + }, + { + path: "/fetch", + loader: () => dfd.promise, + }, + ], + { window: getWindow("/") } + ); + let { container } = render(); - expect(getHtml(container)).toMatch("Num fetchers: 0"); + expect(getHtml(container)).toMatch("Num fetchers: 0"); - fireEvent.click(screen.getByText("Submit (idle)")); - expect(getHtml(container)).toMatch("Num fetchers: 1"); - expect(getHtml(container)).toMatch("Submit (submitting)"); + fireEvent.click(screen.getByText("Load (idle)")); + expect(getHtml(container)).toMatch("Num fetchers: 1"); + expect(getHtml(container)).toMatch("Load (loading)"); - fireEvent.click(screen.getByText("Go to /page")); - await waitFor(() => screen.getByText("Page")); - expect(getHtml(container)).toMatch("Num fetchers: 1"); + fireEvent.click(screen.getByText("Go to /page")); + await waitFor(() => screen.getByText("Page")); + expect(getHtml(container)).toMatch("Num fetchers: 1"); + expect(getHtml(container)).toMatch("Page"); - // Resolve after the navigation and revalidation - dfd.resolve("FETCH"); - await waitFor(() => screen.getByText("Num fetchers: 0")); - }); + // Reject after the navigation - no-op because the fetcher is no longer mounted + dfd.reject(new Error("FETCH ERROR")); + await waitFor(() => screen.getByText("Num fetchers: 0")); + expect(getHtml(container)).toMatch("Page"); + expect(getHtml(container)).not.toMatch( + "Unexpected Application Error!" + ); + expect(getHtml(container)).not.toMatch("FETCH ERROR"); + }); - it("unmounted fetcher.load errors should not bubble up to the UI", async () => { - let dfd = createDeferred(); - let router = createTestRouter( - [ - { - path: "/", - Component() { - let fetchers = useFetchers(); - return ( - <> -
{`Num fetchers: ${fetchers.length}`}
- Go to /page - - - ); - }, - children: [ - { - index: true, - Component() { - let fetcher = useFetcher(); - return ( + it("unmounted/remounted fetcher.load errors should bubble up to the UI", async () => { + let dfd = createDeferred(); + let router = createTestRouter( + [ + { + path: "/", + Component() { + let fetchers = useFetchers(); + return ( + <> +
{`Num fetchers: ${fetchers.length}`}
+ Go to /page + + + ); + }, + children: [ + { + index: true, + Component() { + let fetcher = useFetcher({ key: "me" }); + return ( + <> +

Index

- ); - }, + + ); }, - { - path: "page", - Component() { - return

Page

; - }, + }, + { + path: "page", + Component() { + let fetcher = useFetcher({ key: "me" }); + return ( + <> +

Page

+
{fetcher.data}
+ + ); }, - ], - }, - { - path: "/fetch", - loader: () => dfd.promise, - }, - ], - { window: getWindow("/"), future: { v7_fetcherPersist: true } } - ); - let { container } = render(); + }, + ], + }, + { + path: "/fetch", + loader: () => dfd.promise, + }, + ], + { window: getWindow("/") } + ); + let { container } = render(); - expect(getHtml(container)).toMatch("Num fetchers: 0"); + await waitFor(() => screen.getByText("Index")); + expect(getHtml(container)).toMatch("Num fetchers: 0"); - fireEvent.click(screen.getByText("Load (idle)")); - expect(getHtml(container)).toMatch("Num fetchers: 1"); - expect(getHtml(container)).toMatch("Load (loading)"); + fireEvent.click(screen.getByText("Load (idle)")); + expect(getHtml(container)).toMatch("Num fetchers: 1"); + expect(getHtml(container)).toMatch("Load (loading)"); - fireEvent.click(screen.getByText("Go to /page")); - await waitFor(() => screen.getByText("Page")); - expect(getHtml(container)).toMatch("Num fetchers: 1"); - expect(getHtml(container)).toMatch("Page"); + fireEvent.click(screen.getByText("Go to /page")); + await waitFor(() => screen.getByText("Page")); + expect(getHtml(container)).toMatch("Num fetchers: 1"); + expect(getHtml(container)).toMatch("Page"); - // Reject after the navigation - no-op because the fetcher is no longer mounted - dfd.reject(new Error("FETCH ERROR")); - await waitFor(() => screen.getByText("Num fetchers: 0")); - expect(getHtml(container)).toMatch("Page"); - expect(getHtml(container)).not.toMatch( - "Unexpected Application Error!" - ); - expect(getHtml(container)).not.toMatch("FETCH ERROR"); - }); + // Reject after the navigation - should trigger the error boundary + // because the fetcher is still mounted in the new location + dfd.reject(new Error("FETCH ERROR")); + await waitFor(() => screen.getByText("FETCH ERROR")); + expect(getHtml(container)).toMatch("Unexpected Application Error!"); + }); - it("unmounted/remounted fetcher.load errors should bubble up to the UI", async () => { - let dfd = createDeferred(); - let router = createTestRouter( - [ - { - path: "/", - Component() { - let fetchers = useFetchers(); - return ( - <> -
{`Num fetchers: ${fetchers.length}`}
- Go to /page - - - ); - }, - children: [ - { - index: true, - Component() { - let fetcher = useFetcher({ key: "me" }); - return ( - <> -

Index

- - - ); - }, + it("unmounted fetcher.submit errors should not bubble up to the UI", async () => { + let dfd = createDeferred(); + let router = createTestRouter( + [ + { + path: "/", + Component() { + let fetchers = useFetchers(); + return ( + <> +
{`Num fetchers: ${fetchers.length}`}
+ Go to /page + + + ); + }, + children: [ + { + index: true, + Component() { + let fetcher = useFetcher(); + return ( + + ); }, - { - path: "page", - Component() { - let fetcher = useFetcher({ key: "me" }); - return ( - <> -

Page

-
{fetcher.data}
- - ); - }, + }, + { + path: "page", + Component() { + return

Page

; }, - ], - }, - { - path: "/fetch", - loader: () => dfd.promise, - }, - ], - { window: getWindow("/"), future: { v7_fetcherPersist: true } } - ); - let { container } = render(); - - await waitFor(() => screen.getByText("Index")); - expect(getHtml(container)).toMatch("Num fetchers: 0"); - - fireEvent.click(screen.getByText("Load (idle)")); - expect(getHtml(container)).toMatch("Num fetchers: 1"); - expect(getHtml(container)).toMatch("Load (loading)"); - - fireEvent.click(screen.getByText("Go to /page")); - await waitFor(() => screen.getByText("Page")); - expect(getHtml(container)).toMatch("Num fetchers: 1"); - expect(getHtml(container)).toMatch("Page"); - - // Reject after the navigation - should trigger the error boundary - // because the fetcher is still mounted in the new location - dfd.reject(new Error("FETCH ERROR")); - await waitFor(() => screen.getByText("FETCH ERROR")); - expect(getHtml(container)).toMatch("Unexpected Application Error!"); - }); - - it("unmounted fetcher.submit errors should not bubble up to the UI", async () => { - let dfd = createDeferred(); - let router = createTestRouter( - [ - { - path: "/", - Component() { - let fetchers = useFetchers(); - return ( - <> -
{`Num fetchers: ${fetchers.length}`}
- Go to /page - - - ); }, - children: [ - { - index: true, - Component() { - let fetcher = useFetcher(); - return ( + ], + }, + { + path: "/fetch", + action: () => dfd.promise, + }, + ], + { window: getWindow("/") } + ); + let { container } = render(); + + expect(getHtml(container)).toMatch("Num fetchers: 0"); + + fireEvent.click(screen.getByText("Submit (idle)")); + expect(getHtml(container)).toMatch("Num fetchers: 1"); + expect(getHtml(container)).toMatch("Submit (submitting)"); + + fireEvent.click(screen.getByText("Go to /page")); + await waitFor(() => screen.getByText("Page")); + expect(getHtml(container)).toMatch("Num fetchers: 1"); + expect(getHtml(container)).toMatch("Page"); + + // Reject after the navigation - no-op because the fetcher is no longer mounted + dfd.reject(new Error("FETCH ERROR")); + await waitFor(() => screen.getByText("Num fetchers: 0")); + expect(getHtml(container)).toMatch("Page"); + expect(getHtml(container)).not.toMatch( + "Unexpected Application Error!" + ); + expect(getHtml(container)).not.toMatch("FETCH ERROR"); + }); + + it("unmounted/remounted fetcher.submit errors should bubble up to the UI", async () => { + let dfd = createDeferred(); + let router = createTestRouter( + [ + { + path: "/", + Component() { + let fetchers = useFetchers(); + return ( + <> +
{`Num fetchers: ${fetchers.length}`}
+ Go to /page + + + ); + }, + children: [ + { + index: true, + Component() { + let fetcher = useFetcher({ key: "me" }); + return ( + <> +

Index

- ); - }, + + ); }, - { - path: "page", - Component() { - return

Page

; - }, + }, + { + path: "page", + Component() { + let fetcher = useFetcher({ key: "me" }); + return ( + <> +

Page

+
{fetcher.data}
+ + ); }, - ], - }, - { - path: "/fetch", - action: () => dfd.promise, - }, - ], - { window: getWindow("/"), future: { v7_fetcherPersist: true } } - ); - let { container } = render(); + }, + ], + }, + { + path: "/fetch", + action: () => dfd.promise, + }, + ], + { window: getWindow("/") } + ); + let { container } = render(); - expect(getHtml(container)).toMatch("Num fetchers: 0"); + await waitFor(() => screen.getByText("Index")); + expect(getHtml(container)).toMatch("Num fetchers: 0"); - fireEvent.click(screen.getByText("Submit (idle)")); - expect(getHtml(container)).toMatch("Num fetchers: 1"); - expect(getHtml(container)).toMatch("Submit (submitting)"); + fireEvent.click(screen.getByText("Submit (idle)")); + expect(getHtml(container)).toMatch("Num fetchers: 1"); + expect(getHtml(container)).toMatch("Submit (submitting)"); - fireEvent.click(screen.getByText("Go to /page")); - await waitFor(() => screen.getByText("Page")); - expect(getHtml(container)).toMatch("Num fetchers: 1"); - expect(getHtml(container)).toMatch("Page"); + fireEvent.click(screen.getByText("Go to /page")); + await waitFor(() => screen.getByText("Page")); + expect(getHtml(container)).toMatch("Num fetchers: 1"); + expect(getHtml(container)).toMatch("Page"); - // Reject after the navigation - no-op because the fetcher is no longer mounted - dfd.reject(new Error("FETCH ERROR")); - await waitFor(() => screen.getByText("Num fetchers: 0")); - expect(getHtml(container)).toMatch("Page"); - expect(getHtml(container)).not.toMatch( - "Unexpected Application Error!" - ); - expect(getHtml(container)).not.toMatch("FETCH ERROR"); - }); + // Reject after the navigation - should trigger the error boundary + // because the fetcher is still mounted in the new location + dfd.reject(new Error("FETCH ERROR")); + await waitFor(() => screen.getByText("FETCH ERROR")); + expect(getHtml(container)).toMatch("Unexpected Application Error!"); + }); - it("unmounted/remounted fetcher.submit errors should bubble up to the UI", async () => { - let dfd = createDeferred(); - let router = createTestRouter( - [ - { - path: "/", - Component() { - let fetchers = useFetchers(); - return ( - <> -
{`Num fetchers: ${fetchers.length}`}
- Go to /page - - - ); - }, - children: [ - { - index: true, - Component() { - let fetcher = useFetcher({ key: "me" }); - return ( - <> -

Index

- - - ); - }, - }, - { - path: "page", - Component() { - let fetcher = useFetcher({ key: "me" }); - return ( - <> -

Page

-
{fetcher.data}
- - ); - }, - }, - ], - }, - { - path: "/fetch", - action: () => dfd.promise, - }, - ], - { window: getWindow("/"), future: { v7_fetcherPersist: true } } - ); - let { container } = render(); - - await waitFor(() => screen.getByText("Index")); - expect(getHtml(container)).toMatch("Num fetchers: 0"); - - fireEvent.click(screen.getByText("Submit (idle)")); - expect(getHtml(container)).toMatch("Num fetchers: 1"); - expect(getHtml(container)).toMatch("Submit (submitting)"); - - fireEvent.click(screen.getByText("Go to /page")); - await waitFor(() => screen.getByText("Page")); - expect(getHtml(container)).toMatch("Num fetchers: 1"); - expect(getHtml(container)).toMatch("Page"); - - // Reject after the navigation - should trigger the error boundary - // because the fetcher is still mounted in the new location - dfd.reject(new Error("FETCH ERROR")); - await waitFor(() => screen.getByText("FETCH ERROR")); - expect(getHtml(container)).toMatch("Unexpected Application Error!"); - }); - - it("unmounted fetchers should not revalidate", async () => { - let count = 0; - let loaderDfd = createDeferred(); - let actionDfd = createDeferred(); - let router = createTestRouter( - [ - { - path: "/", - action: () => actionDfd.promise, - Component() { - let [showFetcher, setShowFetcher] = React.useState(true); - let [fetcherData, setFetcherData] = React.useState(null); - let fetchers = useFetchers(); - let actionData = useActionData(); - let navigation = useNavigation(); - - return ( - <> -
- -

{`Navigation State: ${navigation.state}`}

-

{`Action Data: ${actionData}`}

-

{`Active Fetchers: ${fetchers.length}`}

-
- {showFetcher ? ( - { - setFetcherData(data); - setShowFetcher(false); - }} - /> - ) : ( -

{fetcherData}

- )} - - ); - }, + it("unmounted fetchers should not revalidate", async () => { + let count = 0; + let loaderDfd = createDeferred(); + let actionDfd = createDeferred(); + let router = createTestRouter( + [ + { + path: "/", + action: () => actionDfd.promise, + Component() { + let [showFetcher, setShowFetcher] = React.useState(true); + let [fetcherData, setFetcherData] = React.useState(null); + let fetchers = useFetchers(); + let actionData = useActionData(); + let navigation = useNavigation(); + + return ( + <> +
+ +

{`Navigation State: ${navigation.state}`}

+

{`Action Data: ${actionData}`}

+

{`Active Fetchers: ${fetchers.length}`}

+
+ {showFetcher ? ( + { + setFetcherData(data); + setShowFetcher(false); + }} + /> + ) : ( +

{fetcherData}

+ )} + + ); }, - { - path: "/fetch", - async loader() { - count++; - if (count === 1) return await loaderDfd.promise; - throw new Error("Fetcher load called too many times"); - }, + }, + { + path: "/fetch", + async loader() { + count++; + if (count === 1) return await loaderDfd.promise; + throw new Error("Fetcher load called too many times"); }, - ], - { window: getWindow("/"), future: { v7_fetcherPersist: true } } - ); + }, + ], + { window: getWindow("/") } + ); - function FetcherComponent({ onClose }) { - let fetcher = useFetcher(); + function FetcherComponent({ onClose }) { + let fetcher = useFetcher(); - React.useEffect(() => { - if (fetcher.state === "idle" && fetcher.data) { - onClose(fetcher.data); - } - }, [fetcher, onClose]); + React.useEffect(() => { + if (fetcher.state === "idle" && fetcher.data) { + onClose(fetcher.data); + } + }, [fetcher, onClose]); - return ( - <> - -
{`Fetcher State: ${fetcher.state}`}
- - ); - } + return ( + <> + +
{`Fetcher State: ${fetcher.state}`}
+ + ); + } - render(); + render(); - fireEvent.click(screen.getByText("Load Fetcher")); - await waitFor( - () => - screen.getByText("Active Fetchers: 1") && - screen.getByText("Fetcher State: loading") - ); + fireEvent.click(screen.getByText("Load Fetcher")); + await waitFor( + () => + screen.getByText("Active Fetchers: 1") && + screen.getByText("Fetcher State: loading") + ); - loaderDfd.resolve("FETCHER DATA"); - await waitFor( - () => - screen.getByText("FETCHER DATA") && - screen.getByText("Active Fetchers: 0") - ); + loaderDfd.resolve("FETCHER DATA"); + await waitFor( + () => + screen.getByText("FETCHER DATA") && + screen.getByText("Active Fetchers: 0") + ); - fireEvent.click(screen.getByText("Submit Form")); - await waitFor(() => - screen.getByText("Navigation State: submitting") - ); + fireEvent.click(screen.getByText("Submit Form")); + await waitFor(() => screen.getByText("Navigation State: submitting")); - actionDfd.resolve("ACTION"); - await waitFor( - () => - screen.getByText("Navigation State: idle") && - screen.getByText("Active Fetchers: 0") && - screen.getByText("Action Data: ACTION") - ); + actionDfd.resolve("ACTION"); + await waitFor( + () => + screen.getByText("Navigation State: idle") && + screen.getByText("Active Fetchers: 0") && + screen.getByText("Action Data: ACTION") + ); - expect(count).toBe(1); - }); + expect(count).toBe(1); }); }); @@ -6470,7 +6394,7 @@ function testDomRouter( loaderDefer.resolve("LOADER"); // Fetcher does not change the location key. Because no useFetcher() // accessed this key, the fetcher/data doesn't stick around - await waitFor(() => screen.getByText("default,idle,INIT,,idle")); + await waitFor(() => screen.getByText("default,idle,INIT,,")); }); it('uses a fetcher for
', async () => { @@ -6493,7 +6417,7 @@ function testDomRouter( loaderDefer.resolve("LOADER"); // Fetcher does not change the location key. Because no useFetcher() // accessed this key, the fetcher/data doesn't stick around - await waitFor(() => screen.getByText("default,idle,LOADER,,idle")); + await waitFor(() => screen.getByText("default,idle,LOADER,,")); }); it('uses a fetcher for ', async () => { @@ -6509,7 +6433,7 @@ function testDomRouter( loaderDefer.resolve("LOADER"); // Fetcher does not change the location key. Because no useFetcher() // accessed this key, the fetcher/data doesn't stick around - await waitFor(() => screen.getByText("default,idle,INIT,,idle")); + await waitFor(() => screen.getByText("default,idle,INIT,,")); expect(getHtml(container)).toMatch("fetcher:idle:LOADER"); }); @@ -6535,7 +6459,7 @@ function testDomRouter( loaderDefer.resolve("LOADER"); // Fetcher does not change the location key. Because no useFetcher() // accessed this key, the fetcher/data doesn't stick around - await waitFor(() => screen.getByText("default,idle,LOADER,,idle")); + await waitFor(() => screen.getByText("default,idle,LOADER,,")); expect(getHtml(container)).toMatch("fetcher:idle:ACTION:value"); }); }); diff --git a/packages/react-router/__tests__/router/data-strategy-test.ts b/packages/react-router/__tests__/router/data-strategy-test.ts index a498d77b38..cf24413036 100644 --- a/packages/react-router/__tests__/router/data-strategy-test.ts +++ b/packages/react-router/__tests__/router/data-strategy-test.ts @@ -621,9 +621,7 @@ describe("router dataStrategy", () => { await A.loaders.json.resolve(json({ message: "hello json" })); - expect(t.router.state.fetchers.get(key)?.data.message).toBe( - "hello json" - ); + expect(t.fetchers[key].data.message).toBe("hello json"); expect(dataStrategy).toHaveBeenCalledWith( expect.objectContaining({ @@ -662,9 +660,7 @@ describe("router dataStrategy", () => { await A.lazy.json.resolve({ loader: () => ({ message: "hello json" }), }); - expect(t.router.state.fetchers.get(key)?.data.message).toBe( - "hello json" - ); + expect(t.fetchers[key].data.message).toBe("hello json"); expect(dataStrategy).toHaveBeenCalledWith( expect.objectContaining({ request: expect.any(Request), @@ -707,9 +703,7 @@ describe("router dataStrategy", () => { await A.actions.json.resolve(json({ message: "hello json" })); - expect(t.router.state.fetchers.get(key)?.data.message).toBe( - "hello json" - ); + expect(t.fetchers[key].data.message).toBe("hello json"); expect(dataStrategy).toHaveBeenCalledWith( expect.objectContaining({ @@ -752,9 +746,7 @@ describe("router dataStrategy", () => { action: () => ({ message: "hello json" }), }); - expect(t.router.state.fetchers.get(key)?.data.message).toBe( - "hello json" - ); + expect(t.fetchers[key].data.message).toBe("hello json"); expect(dataStrategy).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/packages/react-router/__tests__/router/defer-test.ts b/packages/react-router/__tests__/router/defer-test.ts index d7a60bb2cb..e32a5a5725 100644 --- a/packages/react-router/__tests__/router/defer-test.ts +++ b/packages/react-router/__tests__/router/defer-test.ts @@ -6,7 +6,12 @@ import { defer, } from "../../lib/router"; import { deferredData, trackedPromise } from "./utils/custom-matchers"; -import { cleanup, createDeferred, setup } from "./utils/data-router-setup"; +import { + cleanup, + createDeferred, + getFetcherData, + setup, +} from "./utils/data-router-setup"; import { createFormData, tick } from "./utils/utils"; interface CustomMatchers { @@ -1377,13 +1382,13 @@ describe("deferred data", () => { lazy: dfd.promise, }) ); - expect(t.router.state.fetchers.get(key)).toMatchObject({ + expect(t.fetchers[key]).toMatchObject({ state: "loading", data: undefined, }); await dfd.resolve("2"); - expect(t.router.state.fetchers.get(key)).toMatchObject({ + expect(t.fetchers[key]).toMatchObject({ state: "idle", data: { critical: "1", @@ -1401,7 +1406,7 @@ describe("deferred data", () => { lazy: dfd2.promise, }) ); - expect(t.router.state.fetchers.get(key)).toMatchObject({ + expect(t.fetchers[key]).toMatchObject({ state: "idle", data: { critical: "1", @@ -1410,7 +1415,7 @@ describe("deferred data", () => { }); await dfd2.resolve("4"); - expect(t.router.state.fetchers.get(key)).toMatchObject({ + expect(t.fetchers[key]).toMatchObject({ state: "idle", data: { critical: "3", @@ -1479,7 +1484,7 @@ describe("deferred data", () => { lazy: dfd1.promise, }) ); - expect(t.router.state.fetchers.get(key)).toMatchObject({ + expect(t.fetchers[key]).toMatchObject({ state: "loading", data: undefined, }); @@ -1494,7 +1499,7 @@ describe("deferred data", () => { lazy: dfd2.promise, }) ); - expect(t.router.state.fetchers.get(key)).toMatchObject({ + expect(t.fetchers[key]).toMatchObject({ state: "loading", data: undefined, }); @@ -1504,7 +1509,7 @@ describe("deferred data", () => { await dfd2.resolve("4"); await loaderPromise1; await loaderPromise2; - expect(t.router.state.fetchers.get(key)).toMatchObject({ + expect(t.fetchers[key]).toMatchObject({ state: "idle", data: { critical: "3", @@ -1582,7 +1587,7 @@ describe("deferred data", () => { }); await B.actions.b.resolve("ACTION"); - expect(t.router.state.fetchers.get(key)).toMatchObject({ + expect(t.fetchers[key]).toMatchObject({ state: "loading", data: "ACTION", }); @@ -1628,7 +1633,7 @@ describe("deferred data", () => { lazy: expect.trackedPromise("Yep!"), }, }); - expect(t.router.state.fetchers.get(key)).toMatchObject({ + expect(t.fetchers[key]).toMatchObject({ state: "idle", data: "ACTION", }); @@ -1657,6 +1662,7 @@ describe("deferred data", () => { }, }, }); + let fetcherData = getFetcherData(router); // navigate to root, kicking off a reload of the root loader let key = "key"; @@ -1667,10 +1673,8 @@ describe("deferred data", () => { expect(router.state.loaderData).toEqual({ root: { value: -1 }, }); - expect(router.state.fetchers.get(key)).toMatchObject({ - state: "loading", - data: undefined, - }); + expect(router.getFetcher(key).state).toBe("loading"); + expect(fetcherData.get(key)).toBe(undefined); // Interrupt with a revalidation router.revalidate(); @@ -1683,10 +1687,8 @@ describe("deferred data", () => { expect(router.state.loaderData).toEqual({ root: { value: -1 }, }); - expect(router.state.fetchers.get(key)).toMatchObject({ - state: "loading", - data: undefined, - }); + expect(router.getFetcher(key).state).toBe("loading"); + expect(fetcherData.get(key)).toBe(undefined); // New deferreds should complete the revalidation dfds[2].resolve(2); @@ -1696,10 +1698,8 @@ describe("deferred data", () => { expect(router.state.loaderData).toEqual({ root: { value: expect.trackedPromise(2) }, }); - expect(router.state.fetchers.get(key)).toMatchObject({ - state: "idle", - data: { value: 3 }, - }); + expect(router.getFetcher(key).state).toBe("idle"); + expect(fetcherData.get(key)).toEqual({ value: 3 }); // Assert that both the route loader and fetcher loader were aborted expect(signals[0].aborted).toBe(true); // initial route diff --git a/packages/react-router/__tests__/router/fetchers-test.ts b/packages/react-router/__tests__/router/fetchers-test.ts index 2fa7b1c0b5..7e12a14ce8 100644 --- a/packages/react-router/__tests__/router/fetchers-test.ts +++ b/packages/react-router/__tests__/router/fetchers-test.ts @@ -11,6 +11,7 @@ import { import { cleanup, createDeferred, + getFetcherData, setup, TASK_ROUTES, } from "./utils/data-router-setup"; @@ -64,7 +65,6 @@ function initializeTest(init?: { hydrationData: init?.hydrationData || { loaderData: { root: "ROOT", index: "INDEX" }, }, - future: init?.future, ...(init?.url ? { initialEntries: [init.url] } : {}), }); } @@ -98,25 +98,20 @@ describe("fetchers", () => { loaderData: { root: "ROOT DATA" }, }, }); + let fetcherData = getFetcherData(router); let key = "key"; router.fetch(key, "root", "/"); - expect(router.state.fetchers.get(key)).toEqual({ + expect(router.getFetcher(key)).toEqual({ state: "loading", formMethod: undefined, formEncType: undefined, formData: undefined, - data: undefined, }); await dfd.resolve("DATA"); - expect(router.state.fetchers.get(key)).toEqual({ - state: "idle", - formMethod: undefined, - formEncType: undefined, - formData: undefined, - data: "DATA", - }); + expect(router.getFetcher(key)).toBe(IDLE_FETCHER); + expect(fetcherData.get(key)).toBe("DATA"); expect(router._internalFetchControllers.size).toBe(0); }); @@ -155,7 +150,7 @@ describe("fetchers", () => { expect(B.fetcher.state).toBe("idle"); expect(B.fetcher.data).toBe("B DATA"); - expect(A.fetcher).toBe(B.fetcher); + expect(A.fetcher).toEqual(B.fetcher); }); it("loader submission fetch", async () => { @@ -284,17 +279,17 @@ describe("fetchers", () => { it("gives an idle fetcher before submission", async () => { let t = initializeTest(); let fetcher = t.router.getFetcher("randomKey"); - expect(fetcher).toBe(IDLE_FETCHER); + expect(fetcher).toEqual(IDLE_FETCHER); }); it("removes fetchers", async () => { let t = initializeTest(); let A = await t.fetch("/foo"); await A.loaders.foo.resolve("A"); - expect(t.router.getFetcher(A.key).data).toBe("A"); + expect(t.fetchers[A.key].data).toBe("A"); t.router.deleteFetcher(A.key); - expect(t.router.getFetcher(A.key)).toBe(IDLE_FETCHER); + expect(t.router.getFetcher(A.key)).toEqual(IDLE_FETCHER); }); it("cleans up abort controllers", async () => { @@ -338,9 +333,9 @@ describe("fetchers", () => { }); }); - describe("fetcher removal (w/v7_fetcherPersist)", () => { + describe("fetcher removal ", () => { it("loading fetchers persist until completion", async () => { - let t = initializeTest({ future: { v7_fetcherPersist: true } }); + let t = initializeTest(); let key = "key"; t.router.getFetcher(key); // mount @@ -348,11 +343,11 @@ describe("fetchers", () => { let A = await t.fetch("/foo", key); expect(t.router.state.fetchers.size).toBe(1); - expect(t.router.state.fetchers.get(key)?.state).toBe("loading"); + expect(t.router.getFetcher(key)?.state).toBe("loading"); t.router.deleteFetcher(key); // unmount expect(t.router.state.fetchers.size).toBe(1); - expect(t.router.state.fetchers.get(key)?.state).toBe("loading"); + expect(t.router.getFetcher(key)?.state).toBe("loading"); // Cleaned up on completion await A.loaders.foo.resolve("FOO"); @@ -360,7 +355,7 @@ describe("fetchers", () => { }); it("submitting fetchers persist until completion when removed during submitting phase", async () => { - let t = initializeTest({ future: { v7_fetcherPersist: true } }); + let t = initializeTest(); let key = "key"; expect(t.router.state.fetchers.size).toBe(0); @@ -371,27 +366,27 @@ describe("fetchers", () => { formData: createFormData({}), }); expect(t.router.state.fetchers.size).toBe(1); - expect(t.router.state.fetchers.get(key)?.state).toBe("submitting"); + expect(t.router.getFetcher(key)?.state).toBe("submitting"); t.router.deleteFetcher(key); // unmount expect(t.router.state.fetchers.size).toBe(1); - expect(t.router.state.fetchers.get(key)?.state).toBe("submitting"); + expect(t.router.getFetcher(key)?.state).toBe("submitting"); await A.actions.foo.resolve("FOO"); expect(t.router.state.fetchers.size).toBe(1); - expect(t.router.state.fetchers.get(key)?.state).toBe("loading"); + expect(t.router.getFetcher(key)?.state).toBe("loading"); // Cleaned up on completion await A.loaders.root.resolve("ROOT*"); expect(t.router.state.fetchers.size).toBe(1); - expect(t.router.state.fetchers.get(key)?.state).toBe("loading"); + expect(t.router.getFetcher(key)?.state).toBe("loading"); await A.loaders.index.resolve("INDEX*"); expect(t.router.state.fetchers.size).toBe(0); }); it("submitting fetchers persist until completion when removed during loading phase", async () => { - let t = initializeTest({ future: { v7_fetcherPersist: true } }); + let t = initializeTest(); let key = "key"; t.router.getFetcher(key); // mount @@ -402,27 +397,27 @@ describe("fetchers", () => { formData: createFormData({}), }); expect(t.router.state.fetchers.size).toBe(1); - expect(t.router.state.fetchers.get(key)?.state).toBe("submitting"); + expect(t.router.getFetcher(key)?.state).toBe("submitting"); await A.actions.foo.resolve("FOO"); expect(t.router.state.fetchers.size).toBe(1); - expect(t.router.state.fetchers.get(key)?.state).toBe("loading"); + expect(t.router.getFetcher(key)?.state).toBe("loading"); t.router.deleteFetcher(key); // unmount expect(t.router.state.fetchers.size).toBe(1); - expect(t.router.state.fetchers.get(key)?.state).toBe("loading"); + expect(t.router.getFetcher(key)?.state).toBe("loading"); // Cleaned up on completion await A.loaders.root.resolve("ROOT*"); expect(t.router.state.fetchers.size).toBe(1); - expect(t.router.state.fetchers.get(key)?.state).toBe("loading"); + expect(t.router.getFetcher(key)?.state).toBe("loading"); await A.loaders.index.resolve("INDEX*"); expect(t.router.state.fetchers.size).toBe(0); }); it("unmounted fetcher.load errors/redirects should not be processed", async () => { - let t = initializeTest({ future: { v7_fetcherPersist: true } }); + let t = initializeTest(); t.router.getFetcher("a"); // mount let A = await t.fetch("/foo", "a"); @@ -441,7 +436,7 @@ describe("fetchers", () => { }); it("unmounted fetcher.submit errors/redirects should not be processed", async () => { - let t = initializeTest({ future: { v7_fetcherPersist: true } }); + let t = initializeTest(); t.router.getFetcher("a"); // mount let A = await t.fetch("/foo", "a", { @@ -471,7 +466,7 @@ describe("fetchers", () => { let t = initializeTest(); let A = await t.fetch("/foo"); await A.loaders.foo.reject(new Response(null, { status: 400 })); - expect(A.fetcher).toBe(IDLE_FETCHER); + expect(A.fetcher).toEqual(IDLE_FETCHER); expect(t.router.state.errors).toEqual({ root: new ErrorResponseImpl(400, undefined, ""), }); @@ -484,7 +479,7 @@ describe("fetchers", () => { formData: createFormData({ key: "value" }), }); await A.loaders.foo.reject(new Response(null, { status: 400 })); - expect(A.fetcher).toBe(IDLE_FETCHER); + expect(A.fetcher).toEqual(IDLE_FETCHER); expect(t.router.state.errors).toEqual({ root: new ErrorResponseImpl(400, undefined, ""), }); @@ -497,7 +492,7 @@ describe("fetchers", () => { formData: createFormData({ key: "value" }), }); await A.actions.foo.reject(new Response(null, { status: 400 })); - expect(A.fetcher).toBe(IDLE_FETCHER); + expect(A.fetcher).toEqual(IDLE_FETCHER); expect(t.router.state.errors).toEqual({ root: new ErrorResponseImpl(400, undefined, ""), }); @@ -523,7 +518,7 @@ describe("fetchers", () => { formMethod: "post", formData: createFormData({ key: "value" }), }); - expect(A.fetcher).toBe(IDLE_FETCHER); + expect(A.fetcher).toEqual(IDLE_FETCHER); expect(t.router.state.errors).toEqual({ root: new ErrorResponseImpl( 405, @@ -552,7 +547,7 @@ describe("fetchers", () => { body: "not json", formEncType: "application/json", }); - expect(A.fetcher).toBe(IDLE_FETCHER); + expect(A.fetcher).toEqual(IDLE_FETCHER); expect(t.router.state.errors).toEqual({ root: new ErrorResponseImpl( 400, @@ -595,13 +590,13 @@ describe("fetchers", () => { // If the routeId is not an active match, errors bubble to the root let A = await t.fetch("/error", "key1", "wit"); await A.loaders.error.reject(new Error("Kaboom!")); - expect(t.router.getFetcher("key1")).toBe(IDLE_FETCHER); + expect(t.router.getFetcher("key1")).toEqual(IDLE_FETCHER); expect(t.router.state.errors).toEqual({ root: new Error("Kaboom!"), }); await t.fetch("/not-found", "key2", "wit"); - expect(t.router.getFetcher("key2")).toBe(IDLE_FETCHER); + expect(t.router.getFetcher("key2")).toEqual(IDLE_FETCHER); expect(t.router.state.errors).toEqual({ root: new ErrorResponseImpl( 404, @@ -617,7 +612,7 @@ describe("fetchers", () => { let C = await t.fetch("/error", "key3", "wit"); await C.loaders.error.reject(new Error("Kaboom!")); - expect(t.router.getFetcher("key3")).toBe(IDLE_FETCHER); + expect(t.router.getFetcher("key3")).toEqual(IDLE_FETCHER); expect(t.router.state.errors).toEqual({ wit: new Error("Kaboom!"), }); @@ -626,7 +621,7 @@ describe("fetchers", () => { formMethod: "post", formData: createFormData({ key: "value" }), }); - expect(t.router.getFetcher("key4")).toBe(IDLE_FETCHER); + expect(t.router.getFetcher("key4")).toEqual(IDLE_FETCHER); expect(t.router.state.errors).toEqual({ wit: new ErrorResponseImpl( 404, @@ -637,7 +632,7 @@ describe("fetchers", () => { }); await t.fetch("/not-found", "key5", "wit"); - expect(t.router.getFetcher("key5")).toBe(IDLE_FETCHER); + expect(t.router.getFetcher("key5")).toEqual(IDLE_FETCHER); expect(t.router.state.errors).toEqual({ wit: new ErrorResponseImpl( 404, @@ -653,13 +648,13 @@ describe("fetchers", () => { let E = await t.fetch("/error", "key6", "witout"); await E.loaders.error.reject(new Error("Kaboom!")); - expect(t.router.getFetcher("key6")).toBe(IDLE_FETCHER); + expect(t.router.getFetcher("key6")).toEqual(IDLE_FETCHER); expect(t.router.state.errors).toEqual({ root: new Error("Kaboom!"), }); await t.fetch("/not-found", "key7", "witout"); - expect(t.router.getFetcher("key7")).toBe(IDLE_FETCHER); + expect(t.router.getFetcher("key7")).toEqual(IDLE_FETCHER); expect(t.router.state.errors).toEqual({ root: new ErrorResponseImpl( 404, @@ -676,7 +671,7 @@ describe("fetchers", () => { let t = initializeTest(); let A = await t.fetch("/foo"); await A.loaders.foo.reject(new Error("Kaboom!")); - expect(A.fetcher).toBe(IDLE_FETCHER); + expect(A.fetcher).toEqual(IDLE_FETCHER); expect(t.router.state.errors).toEqual({ root: new Error("Kaboom!"), }); @@ -689,7 +684,7 @@ describe("fetchers", () => { formData: createFormData({ key: "value" }), }); await A.loaders.foo.reject(new Error("Kaboom!")); - expect(A.fetcher).toBe(IDLE_FETCHER); + expect(A.fetcher).toEqual(IDLE_FETCHER); expect(t.router.state.errors).toEqual({ root: new Error("Kaboom!"), }); @@ -702,7 +697,7 @@ describe("fetchers", () => { formData: createFormData({ key: "value" }), }); await A.actions.foo.reject(new Error("Kaboom!")); - expect(A.fetcher).toBe(IDLE_FETCHER); + expect(A.fetcher).toEqual(IDLE_FETCHER); expect(t.router.state.errors).toEqual({ root: new Error("Kaboom!"), }); @@ -717,7 +712,7 @@ describe("fetchers", () => { let A = await t.fetch("/foo"); let B = await A.loaders.foo.redirect("/bar"); - expect(t.router.getFetcher(A.key)).toBe(A.fetcher); + expect(t.router.getFetcher(A.key)).toEqual(A.fetcher); expect(t.router.state.navigation.state).toBe("loading"); expect(t.router.state.navigation.location?.pathname).toBe("/bar"); @@ -743,7 +738,7 @@ describe("fetchers", () => { }); let B = await A.loaders.foo.redirect("/bar"); - expect(t.router.getFetcher(A.key)).toBe(A.fetcher); + expect(t.router.getFetcher(A.key)).toEqual(A.fetcher); expect(t.router.state.navigation.state).toBe("loading"); expect(t.router.state.navigation.location?.pathname).toBe("/bar"); @@ -902,14 +897,14 @@ describe("fetchers", () => { formData: createFormData({ key: "value" }), }); await A.actions.foo.resolve("A ACTION"); - expect(t.router.getFetcher(key).data).toBe("A ACTION"); + expect(t.fetchers[key].data).toBe("A ACTION"); let B = await t.fetch("/foo", key, { formMethod: "post", formData: createFormData({ key: "value" }), }); expect(A.loaders.foo.signal.aborted).toBe(true); - expect(t.router.getFetcher(key).data).toBe("A ACTION"); + expect(t.fetchers[key].data).toBe("A ACTION"); await A.loaders.root.resolve("A ROOT LOADER"); await A.loaders.foo.resolve("A LOADER"); @@ -922,10 +917,10 @@ describe("fetchers", () => { expect(B.actions.foo.signal.aborted).toBe(true); await B.actions.foo.resolve("B ACTION"); - expect(t.router.getFetcher(key).data).toBe("A ACTION"); + expect(t.fetchers[key].data).toBe("A ACTION"); await C.actions.foo.resolve("C ACTION"); - expect(t.router.getFetcher(key).data).toBe("C ACTION"); + expect(t.fetchers[key].data).toBe("C ACTION"); await B.loaders.root.resolve("B ROOT LOADER"); await B.loaders.foo.resolve("B LOADER"); @@ -933,7 +928,7 @@ describe("fetchers", () => { await C.loaders.root.resolve("C ROOT LOADER"); await C.loaders.foo.resolve("C LOADER"); - expect(t.router.getFetcher(key).data).toBe("C ACTION"); + expect(t.fetchers[key].data).toBe("C ACTION"); expect(t.router.state.loaderData.foo).toBe("C LOADER"); }); }); @@ -962,7 +957,7 @@ describe("fetchers", () => { await Ak1.actions.foo.resolve("A ACTION"); await Bk2.actions.foo.resolve("B ACTION"); - expect(t.router.getFetcher(k2).data).toBe("B ACTION"); + expect(t.fetchers[k2].data).toBe("B ACTION"); let Ck1 = await t.fetch("/foo", k1, { formMethod: "post", @@ -983,7 +978,7 @@ describe("fetchers", () => { await Ck1.loaders.root.resolve("C ROOT LOADER"); await Ck1.loaders.foo.resolve("C LOADER"); - expect(t.router.getFetcher(k1).data).toBe("C ACTION"); + expect(t.fetchers[k1].data).toBe("C ACTION"); expect(t.router.state.loaderData.foo).toBe("C LOADER"); }); }); @@ -1515,8 +1510,8 @@ describe("fetchers", () => { formData: createFormData({ key: "value" }), }); await A.actions.foo.resolve("A ACTION"); - expect(t.router.state.fetchers.get(key)?.state).toBe("loading"); - expect(t.router.state.fetchers.get(key)?.data).toBe("A ACTION"); + expect(t.router.getFetcher(key)?.state).toBe("loading"); + expect(t.fetchers[key]?.data).toBe("A ACTION"); // Interrupting the actionReload should cause the next load to call all loaders let B = await t.navigate("/bar"); await B.loaders.root.resolve("ROOT*"); @@ -1530,8 +1525,8 @@ describe("fetchers", () => { bar: "BAR", }, }); - expect(t.router.state.fetchers.get(key)?.state).toBe("idle"); - expect(t.router.state.fetchers.get(key)?.data).toBe("A ACTION"); + expect(t.router.getFetcher(key)?.state).toBe("idle"); + expect(t.fetchers[key]?.data).toBe("A ACTION"); }); it("forces all loaders to revalidate on interrupted fetcher submissionRedirect", async () => { @@ -1542,7 +1537,7 @@ describe("fetchers", () => { formData: createFormData({ key: "value" }), }); await A.actions.foo.redirect("/baz"); - expect(t.router.state.fetchers.get(key)?.state).toBe("loading"); + expect(t.router.getFetcher(key)?.state).toBe("loading"); // Interrupting the actionReload should cause the next load to call all loaders let B = await t.navigate("/bar"); await B.loaders.root.resolve("ROOT*"); @@ -1555,8 +1550,8 @@ describe("fetchers", () => { bar: "BAR", }, }); - expect(t.router.state.fetchers.get(key)?.state).toBe("idle"); - expect(t.router.state.fetchers.get(key)?.data).toBeUndefined(); + expect(t.router.getFetcher(key)?.state).toBe("idle"); + expect(t.fetchers[key]?.data).toBeUndefined(); }); }); @@ -1574,7 +1569,7 @@ describe("fetchers", () => { // The fetcher loader redirect should be ignored await A.loaders.foo.redirect("/baz"); - expect(t.router.state.fetchers.get(key)?.state).toBe("idle"); + expect(t.router.getFetcher(key)?.state).toBe("idle"); await B.loaders.bar.resolve("BAR"); expect(t.router.state).toMatchObject({ @@ -1585,8 +1580,8 @@ describe("fetchers", () => { bar: "BAR", }, }); - expect(t.router.state.fetchers.get(key)?.state).toBe("idle"); - expect(t.router.state.fetchers.get(key)?.data).toBeUndefined(); + expect(t.router.getFetcher(key)?.state).toBe("idle"); + expect(t.fetchers[key]?.data).toBeUndefined(); }); }); @@ -1605,7 +1600,7 @@ describe("fetchers", () => { // This redirect should be ignored await A.actions.foo.redirect("/baz"); - expect(t.router.state.fetchers.get(key)?.state).toBe("idle"); + expect(t.router.getFetcher(key)?.state).toBe("idle"); await B.loaders.root.resolve("ROOT*"); await B.loaders.bar.resolve("BAR"); @@ -1617,8 +1612,8 @@ describe("fetchers", () => { bar: "BAR", }, }); - expect(t.router.state.fetchers.get(key)?.state).toBe("idle"); - expect(t.router.state.fetchers.get(key)?.data).toBeUndefined(); + expect(t.router.getFetcher(key)?.state).toBe("idle"); + expect(t.fetchers[key]?.data).toBeUndefined(); }); it("ignores submission redirect navigation if preceded by a normal GET navigation (w/o loaders)", async () => { @@ -1658,15 +1653,15 @@ describe("fetchers", () => { // This redirect should be ignored await A.actions.foo.redirect("/baz"); - expect(t.router.state.fetchers.get(key)?.state).toBe("idle"); + expect(t.router.getFetcher(key)?.state).toBe("idle"); expect(t.router.state).toMatchObject({ navigation: IDLE_NAVIGATION, location: { pathname: "/bar" }, loaderData: {}, }); - expect(t.router.state.fetchers.get(key)?.state).toBe("idle"); - expect(t.router.state.fetchers.get(key)?.data).toBeUndefined(); + expect(t.router.getFetcher(key)?.state).toBe("idle"); + expect(t.fetchers[key]?.data).toBeUndefined(); }); }); @@ -1688,7 +1683,7 @@ describe("fetchers", () => { // The fetcher loader redirect should be ignored await A.loaders.foo.redirect("/baz"); - expect(t.router.state.fetchers.get(key)?.state).toBe("loading"); + expect(t.router.getFetcher(key)?.state).toBe("loading"); // The navigation should trigger the fetcher to revalidate since it's // not yet "completed". If it returns data this time that should be @@ -1706,8 +1701,8 @@ describe("fetchers", () => { bar: "BAR", }, }); - expect(t.router.state.fetchers.get(key)?.state).toBe("idle"); - expect(t.router.state.fetchers.get(key)?.data).toBe("FOO"); + expect(t.router.getFetcher(key)?.state).toBe("idle"); + expect(t.fetchers[key]?.data).toBe("FOO"); }); it("processes second fetcher load redirect after interruption by normal POST navigation", async () => { @@ -1729,7 +1724,7 @@ describe("fetchers", () => { navigation: { location: { pathname: "/bar" } }, location: { pathname: "/" }, }); - expect(t.router.state.fetchers.get(key)?.state).toBe("loading"); + expect(t.router.getFetcher(key).state).toBe("loading"); // The navigation should trigger the fetcher to revalidate since it's // not yet "completed". If it redirects again we should follow that @@ -1746,7 +1741,7 @@ describe("fetchers", () => { root: "ROOT", }, }); - expect(t.router.state.fetchers.get(key)?.state).toBe("loading"); + expect(t.router.getFetcher(key).state).toBe("loading"); // The fetcher should not revalidate here since it triggered the redirect await C.loaders.root.resolve("ROOT**"); @@ -1759,8 +1754,8 @@ describe("fetchers", () => { foobar: "FOOBAR", }, }); - expect(t.router.state.fetchers.get(key)?.state).toBe("idle"); - expect(t.router.state.fetchers.get(key)?.data).toBe(undefined); + expect(t.router.getFetcher(key).state).toBe("idle"); + expect(t.fetchers[key]?.data).toBe(undefined); }); }); @@ -1783,8 +1778,8 @@ describe("fetchers", () => { navigation: { location: { pathname: "/baz" } }, location: { pathname: "/" }, }); - expect(t.router.state.fetchers.get(keyA)?.state).toBe("loading"); - expect(t.router.state.fetchers.get(keyB)?.state).toBe("loading"); + expect(t.router.getFetcher(keyA)?.state).toBe("loading"); + expect(t.router.getFetcher(keyB)?.state).toBe("loading"); // The original fetch load redirect should be ignored await A.loaders.foo.redirect("/foo/bar"); @@ -1792,8 +1787,8 @@ describe("fetchers", () => { navigation: { location: { pathname: "/baz" } }, location: { pathname: "/" }, }); - expect(t.router.state.fetchers.get(keyA)?.state).toBe("idle"); - expect(t.router.state.fetchers.get(keyB)?.state).toBe("loading"); + expect(t.router.getFetcher(keyA)?.state).toBe("idle"); + expect(t.router.getFetcher(keyB)?.state).toBe("loading"); // Resolve the navigation loader await C.loaders.baz.resolve("BAZ"); @@ -1805,8 +1800,8 @@ describe("fetchers", () => { baz: "BAZ", }, }); - expect(t.router.state.fetchers.get(keyA)?.state).toBe("idle"); - expect(t.router.state.fetchers.get(keyB)?.state).toBe("idle"); + expect(t.router.getFetcher(keyA)?.state).toBe("idle"); + expect(t.router.getFetcher(keyB)?.state).toBe("idle"); }); }); }); @@ -1837,13 +1832,13 @@ describe("fetchers", () => { await C.actions.tasks.resolve("TASKS ACTION"); // Fetcher should go back into a loading state - expect(t.router.state.fetchers.get(key1)?.state).toBe("loading"); + expect(t.router.getFetcher(key1)?.state).toBe("loading"); // Resolve navigation loaders + fetcher loader await C.loaders.root.resolve("ROOT*"); await C.loaders.tasks.resolve("TASKS LOADER"); await C.loaders.tasksId.resolve("TASKS ID*"); - expect(t.router.state.fetchers.get(key1)).toMatchObject({ + expect(t.fetchers[key1]).toMatchObject({ state: "idle", data: "TASKS ID*", }); @@ -1856,7 +1851,7 @@ describe("fetchers", () => { await D.actions.tasksId.resolve("TASKS 3"); await D.loaders.root.resolve("ROOT**"); await D.loaders.tasks.resolve("TASKS**"); - expect(t.router.state.fetchers.get(key1)).toMatchObject({ + expect(t.fetchers[key1]).toMatchObject({ state: "idle", data: "TASKS 3", }); @@ -1870,7 +1865,7 @@ describe("fetchers", () => { await E.actions.tasks.resolve("TASKS***"); // Remains the same state as it was after the submission - expect(t.router.state.fetchers.get(key1)).toMatchObject({ + expect(t.fetchers[key1]).toMatchObject({ state: "idle", data: "TASKS 3", }); @@ -1899,13 +1894,13 @@ describe("fetchers", () => { let D = await C.actions.tasks.redirect("/", undefined, undefined, [ "tasksId", ]); - expect(t.router.state.fetchers.get(key)?.state).toBe("loading"); + expect(t.router.getFetcher(key)?.state).toBe("loading"); // Resolve navigation loaders + fetcher loader await D.loaders.root.resolve("ROOT*"); await D.loaders.index.resolve("INDEX*"); await D.loaders.tasksId.resolve("TASKS ID*"); - expect(t.router.state.fetchers.get(key)).toMatchObject({ + expect(t.fetchers[key]).toMatchObject({ state: "idle", data: "TASKS ID*", }); @@ -1933,12 +1928,12 @@ describe("fetchers", () => { // Reject the action await C.actions.tasks.reject(new Error("Kaboom!")); - expect(t.router.state.fetchers.get(key)?.state).toBe("loading"); + expect(t.router.getFetcher(key)?.state).toBe("loading"); // Resolve navigation loaders + fetcher loader await C.loaders.root.resolve("ROOT*"); await C.loaders.tasksId.resolve("TASKS ID*"); - expect(t.router.state.fetchers.get(key)).toMatchObject({ + expect(t.fetchers[key]).toMatchObject({ state: "idle", data: "TASKS ID*", }); @@ -1959,7 +1954,7 @@ describe("fetchers", () => { let A = await t.fetch("/?index", key); await A.loaders.index.resolve("FETCH 1"); - expect(t.router.state.fetchers.get(key)).toMatchObject({ + expect(t.fetchers[key]).toMatchObject({ state: "idle", data: "FETCH 1", }); @@ -1971,7 +1966,7 @@ describe("fetchers", () => { root: "ROOT 2", tasksId: "TASK 2", }); - expect(t.router.state.fetchers.get(key)).toMatchObject({ + expect(t.fetchers[key]).toMatchObject({ state: "idle", data: "FETCH 1", }); @@ -1993,7 +1988,7 @@ describe("fetchers", () => { let A = await t.fetch("/?index", key); await A.loaders.index.resolve("FETCH 1"); - expect(t.router.state.fetchers.get(key)).toMatchObject({ + expect(t.fetchers[key]).toMatchObject({ state: "idle", data: "FETCH 1", }); @@ -2005,7 +2000,7 @@ describe("fetchers", () => { root: "ROOT 2", tasksId: "TASK 2", }); - expect(t.router.state.fetchers.get(key)).toMatchObject({ + expect(t.fetchers[key]).toMatchObject({ state: "idle", data: "FETCH 1", }); @@ -2022,7 +2017,7 @@ describe("fetchers", () => { let A = await t.fetch("/", key); await A.loaders.root.resolve("ROOT FETCH"); - expect(t.router.state.fetchers.get(key)).toMatchObject({ + expect(t.fetchers[key]).toMatchObject({ state: "idle", data: "ROOT FETCH", }); @@ -2033,7 +2028,7 @@ describe("fetchers", () => { root: "ROOT", tasks: "TASKS", }); - expect(t.router.state.fetchers.get(key)).toMatchObject({ + expect(t.fetchers[key]).toMatchObject({ state: "idle", data: "ROOT FETCH", }); @@ -2073,19 +2068,19 @@ describe("fetchers", () => { loaderData: { root: count }, }, }); + let fetcherData = getFetcherData(router); + router.initialize(); expect(router.state.loaderData).toMatchObject({ root: 0, }); - expect(router.getFetcher(key)).toBe(IDLE_FETCHER); + expect(router.getFetcher(key)).toEqual(IDLE_FETCHER); // Fetch from a different route router.fetch(key, "root", "/fetch"); await tick(); - expect(router.getFetcher(key)).toMatchObject({ - state: "idle", - data: 1, - }); + expect(router.getFetcher(key)).toEqual(IDLE_FETCHER); + expect(fetcherData.get(key)).toBe(1); // Post to the current route router.navigate("/two/three", { @@ -2096,10 +2091,9 @@ describe("fetchers", () => { expect(router.state.loaderData).toMatchObject({ root: 2, }); - expect(router.getFetcher(key)).toMatchObject({ - state: "idle", - data: 1, - }); + expect(router.getFetcher(key)).toEqual(IDLE_FETCHER); + expect(fetcherData.get(key)).toBe(1); + expect(shouldRevalidate.mock.calls[0][0]).toMatchInlineSnapshot(` { "actionResult": null, @@ -2145,7 +2139,7 @@ describe("fetchers", () => { let A = await t.fetch("/tasks/1", key); await A.loaders.tasksId.resolve("ROOT FETCH"); - expect(t.router.state.fetchers.get(key)).toMatchObject({ + expect(t.fetchers[key]).toMatchObject({ state: "idle", data: "ROOT FETCH", }); @@ -2171,7 +2165,7 @@ describe("fetchers", () => { root: new Error("Fetcher error"), }, }); - expect(t.router.state.fetchers.get(key)).toBe(undefined); + expect(t.router.getFetcher(key)).toBe(IDLE_FETCHER); }); it("revalidates fetchers on fetcher action submissions", async () => { @@ -2186,7 +2180,7 @@ describe("fetchers", () => { // Load a fetcher let A = await t.fetch("/tasks/1", key); await A.loaders.tasksId.resolve("TASKS ID"); - expect(t.router.state.fetchers.get(key)).toMatchObject({ + expect(t.fetchers[key]).toMatchObject({ state: "idle", data: "TASKS ID", }); @@ -2197,22 +2191,22 @@ describe("fetchers", () => { formData: createFormData({}), }); t.shimHelper(C.loaders, "fetch", "loader", "tasksId"); - expect(t.router.state.fetchers.get(key)).toMatchObject({ + expect(t.fetchers[key]).toMatchObject({ state: "idle", data: "TASKS ID", }); - expect(t.router.state.fetchers.get(actionKey)).toMatchObject({ + expect(t.fetchers[actionKey]).toMatchObject({ state: "submitting", }); // After action resolves, both fetchers go into a loading state, with // the load fetcher still reflecting it's stale data await C.actions.tasks.resolve("TASKS ACTION"); - expect(t.router.state.fetchers.get(key)).toMatchObject({ + expect(t.fetchers[key]).toMatchObject({ state: "loading", data: "TASKS ID", }); - expect(t.router.state.fetchers.get(actionKey)).toMatchObject({ + expect(t.fetchers[actionKey]).toMatchObject({ state: "loading", data: "TASKS ACTION", }); @@ -2226,11 +2220,11 @@ describe("fetchers", () => { root: "ROOT*", index: "INDEX*", }); - expect(t.router.state.fetchers.get(key)).toMatchObject({ + expect(t.fetchers[key]).toMatchObject({ state: "idle", data: "TASKS ID*", }); - expect(t.router.state.fetchers.get(actionKey)).toMatchObject({ + expect(t.fetchers[actionKey]).toMatchObject({ state: "idle", data: "TASKS ACTION", }); @@ -2248,7 +2242,7 @@ describe("fetchers", () => { // Trigger a fetch from the index route let A = await t.fetch("/tasks/1", key, "index"); await A.loaders.tasksId.resolve("TASKS"); - expect(t.router.state.fetchers.get(key)).toMatchObject({ + expect(t.fetchers[key]).toMatchObject({ state: "idle", data: "TASKS", }); @@ -2264,7 +2258,7 @@ describe("fetchers", () => { // Fetcher should remain in an idle state since it's calling route is // being removed - expect(t.router.state.fetchers.get(key)).toMatchObject({ + expect(t.fetchers[key]).toMatchObject({ state: "idle", data: "TASKS", }); @@ -2276,7 +2270,7 @@ describe("fetchers", () => { expect(t.router.state.location.pathname).toBe("/tasks"); // Fetcher never got called - expect(t.router.state.fetchers.get(key)).toMatchObject({ + expect(t.fetchers[key]).toMatchObject({ state: "idle", data: "TASKS", }); @@ -2332,7 +2326,7 @@ describe("fetchers", () => { let keyA = "a"; let A = await t.fetch("/fetch-a", keyA); await A.loaders.fetchA.resolve("A"); - expect(t.router.state.fetchers.get(keyA)).toMatchObject({ + expect(t.fetchers[keyA]).toMatchObject({ state: "idle", data: "A", }); @@ -2340,19 +2334,25 @@ describe("fetchers", () => { let keyB = "b"; let B = await t.fetch("/fetch-b", keyB); await B.loaders.fetchB.resolve("B"); - expect(t.router.state.fetchers.get(keyB)).toMatchObject({ + expect(t.fetchers[keyB]).toMatchObject({ state: "idle", data: "B", }); // Fetch again for B let B2 = await t.fetch("/fetch-b", keyB); - expect(t.router.state.fetchers.get(keyB)?.state).toBe("loading"); + expect(t.fetchers[keyB]).toMatchObject({ + state: "loading", + data: "B", + }); // Start another fetcher which will not resolve prior to the action let keyC = "c"; let C = await t.fetch("/fetch-c", keyC); - expect(t.router.state.fetchers.get(keyC)?.state).toBe("loading"); + expect(t.fetchers[keyC]).toMatchObject({ + state: "loading", + data: undefined, + }); // Navigation should cancel fetcher and since it has no data // shouldRevalidate should be ignored on subsequent fetch @@ -2371,32 +2371,32 @@ describe("fetchers", () => { expect(B.loaders.fetchB.signal.aborted).toBe(false); expect(B2.loaders.fetchB.signal.aborted).toBe(true); expect(C.loaders.fetchC.signal.aborted).toBe(true); - expect(t.router.state.fetchers.get(keyA)?.state).toBe("idle"); - expect(t.router.state.fetchers.get(keyB)?.state).toBe("loading"); - expect(t.router.state.fetchers.get(keyC)?.state).toBe("loading"); + expect(t.fetchers[keyA].state).toBe("idle"); + expect(t.fetchers[keyB].state).toBe("loading"); + expect(t.fetchers[keyC].state).toBe("loading"); await B.loaders.fetchB.resolve("B"); // ignored due to abort await C.loaders.fetchC.resolve("C"); // ignored due to abort // Resolve the action await D.actions.action.resolve("ACTION"); expect(t.router.state.navigation.state).toBe("loading"); - expect(t.router.state.fetchers.get(keyA)?.state).toBe("idle"); - expect(t.router.state.fetchers.get(keyB)?.state).toBe("loading"); - expect(t.router.state.fetchers.get(keyC)?.state).toBe("loading"); + expect(t.fetchers[keyA].state).toBe("idle"); + expect(t.fetchers[keyB].state).toBe("loading"); + expect(t.fetchers[keyC].state).toBe("loading"); // Resolve fetcher loader await D.loaders.fetchB.resolve("B2"); await D.loaders.fetchC.resolve("C"); expect(t.router.state.navigation.state).toBe("idle"); - expect(t.router.state.fetchers.get(keyA)).toMatchObject({ + expect(t.fetchers[keyA]).toMatchObject({ state: "idle", data: "A", }); - expect(t.router.state.fetchers.get(keyB)).toMatchObject({ + expect(t.fetchers[keyB]).toMatchObject({ state: "idle", data: "B2", }); - expect(t.router.state.fetchers.get(keyC)).toMatchObject({ + expect(t.fetchers[keyC]).toMatchObject({ state: "idle", data: "C", }); @@ -2425,18 +2425,22 @@ describe("fetchers", () => { await C.actions.tasks.resolve("TASKS ACTION"); // Fetcher should go back into a loading state - expect(t.router.state.fetchers.get(key1)).toMatchObject({ + expect(t.fetchers[key1]).toMatchObject({ state: "loading", data: "TASKS 1", }); // Delete fetcher in the middle of the revalidation t.router.deleteFetcher(key1); - expect(t.router.state.fetchers.get(key1)).toBeUndefined(); + expect(t.fetchers[key1]).toMatchObject({ + state: "loading", + data: "TASKS 1", + }); // Resolve navigation loaders await C.loaders.root.resolve("ROOT*"); await C.loaders.tasks.resolve("TASKS LOADER"); + await C.loaders.tasksId.resolve("TASKS 2"); expect(t.router.state).toMatchObject({ actionData: { @@ -2448,6 +2452,10 @@ describe("fetchers", () => { root: "ROOT*", }, }); + expect(t.fetchers[key1]).toMatchObject({ + state: "idle", + data: "TASKS 2", + }); expect(t.router.state.fetchers.size).toBe(0); }); @@ -2464,25 +2472,33 @@ describe("fetchers", () => { await A.loaders.tasksId.resolve("TASKS 1"); // Submission navigation to trigger revalidations - let C = await t.navigate("/tasks", { - formMethod: "post", - formData: createFormData({}), - }); + let C = await t.navigate( + "/tasks", + { + formMethod: "post", + formData: createFormData({}), + }, + ["tasksId"] + ); await C.actions.tasks.resolve("TASKS ACTION"); // Fetcher should go back into a loading state - expect(t.router.state.fetchers.get(key1)).toMatchObject({ + expect(t.fetchers[key1]).toMatchObject({ state: "loading", data: "TASKS 1", }); // Delete fetcher in the middle of the revalidation t.router.deleteFetcher(key1); - expect(t.router.state.fetchers.get(key1)).toBeUndefined(); + expect(t.fetchers[key1]).toMatchObject({ + state: "loading", + data: "TASKS 1", + }); // Resolve navigation action/loaders await C.loaders.root.resolve("ROOT*"); await C.loaders.tasks.resolve("TASKS LOADER"); + await C.loaders.tasksId.resolve("TASKS 2"); expect(t.router.state).toMatchObject({ errors: null, @@ -2495,6 +2511,10 @@ describe("fetchers", () => { root: "ROOT*", }, }); + expect(t.fetchers[key1]).toMatchObject({ + state: "idle", + data: "TASKS 2", + }); expect(t.router.state.fetchers.size).toBe(0); }); @@ -2511,21 +2531,25 @@ describe("fetchers", () => { await A.loaders.tasksId.resolve("TASKS 1"); // Trigger revalidations - let C = await t.revalidate(); + let C = await t.revalidate("navigation", "tasksId"); // Fetcher should not go back into a loading state since it's a revalidation - expect(t.router.state.fetchers.get(key1)).toMatchObject({ + expect(t.fetchers[key1]).toMatchObject({ state: "idle", data: "TASKS 1", }); // Delete fetcher in the middle of the revalidation t.router.deleteFetcher(key1); - expect(t.router.state.fetchers.get(key1)).toBeUndefined(); + expect(t.fetchers[key1]).toMatchObject({ + state: "idle", + data: "TASKS 1", + }); // Resolve navigation loaders await C.loaders.root.resolve("ROOT*"); await C.loaders.index.resolve("INDEX*"); + await C.loaders.tasksId.resolve("TASKS 2"); expect(t.router.state).toMatchObject({ errors: null, @@ -2534,6 +2558,10 @@ describe("fetchers", () => { index: "INDEX*", }, }); + expect(t.fetchers[key1]).toMatchObject({ + state: "idle", + data: "TASKS 2", + }); expect(t.router.state.fetchers.size).toBe(0); }); @@ -2559,18 +2587,21 @@ describe("fetchers", () => { // After action resolves, both fetchers go into a loading state, with // the load fetcher still reflecting it's stale data await C.actions.tasks.resolve("TASKS ACTION"); - expect(t.router.state.fetchers.get(key)).toMatchObject({ + expect(t.fetchers[key]).toMatchObject({ state: "loading", data: "TASKS ID", }); - expect(t.router.state.fetchers.get(actionKey)).toMatchObject({ + expect(t.router.getFetcher(actionKey)).toMatchObject({ state: "loading", data: "TASKS ACTION", }); // Delete fetcher in the middle of the revalidation t.router.deleteFetcher(key); - expect(t.router.state.fetchers.get(key)).toBeUndefined(); + expect(t.fetchers[key]).toMatchObject({ + state: "loading", + data: "TASKS ID", + }); // Resolve only active route loaders since fetcher was deleted await C.loaders.root.resolve("ROOT*"); @@ -2580,8 +2611,8 @@ describe("fetchers", () => { root: "ROOT*", index: "INDEX*", }); - expect(t.router.state.fetchers.get(key)).toBe(undefined); - expect(t.router.state.fetchers.get(actionKey)).toMatchObject({ + expect(t.router.getFetcher(key)).toBe(IDLE_FETCHER); + expect(t.fetchers[actionKey]).toMatchObject({ state: "idle", data: "TASKS ACTION", }); @@ -2630,8 +2661,8 @@ describe("fetchers", () => { // After action resolves, both fetchers go into a loading state await B.actions.action.resolve("ACTION"); - expect(t.router.state.fetchers.get(key)?.state).toBe("loading"); - expect(t.router.state.fetchers.get(actionKey)?.state).toBe("loading"); + expect(t.router.getFetcher(key)?.state).toBe("loading"); + expect(t.router.getFetcher(actionKey)?.state).toBe("loading"); // Remove the submitting fetcher (assume it's component unmounts) t.router.deleteFetcher(actionKey); @@ -2640,11 +2671,11 @@ describe("fetchers", () => { await B.loaders.fetch.resolve("FETCH*"); expect(t.router.state.loaderData).toEqual({ home: "HOME*" }); - expect(t.router.state.fetchers.get(key)).toMatchObject({ + expect(t.fetchers[key]).toMatchObject({ state: "idle", data: "FETCH*", }); - expect(t.router.state.fetchers.get(actionKey)).toBeUndefined(); + expect(t.router.getFetcher(actionKey)).toBe(IDLE_FETCHER); }); it("does not call shouldRevalidate on POST navigation if fetcher has not yet loaded", async () => { @@ -2681,7 +2712,7 @@ describe("fetchers", () => { let key = "key"; let A = await t.fetch("/fetch", key, "root"); - expect(t.router.state.fetchers.get(key)?.state).toBe("loading"); + expect(t.router.getFetcher(key)?.state).toBe("loading"); // This should trigger an automatic revalidation of the fetcher since it // hasn't loaded yet @@ -2691,18 +2722,18 @@ describe("fetchers", () => { ["fetch"] ); await B.actions.page.resolve("ACTION"); - expect(t.router.state.fetchers.get(key)?.state).toBe("loading"); + expect(t.router.getFetcher(key)?.state).toBe("loading"); expect(A.loaders.fetch.signal.aborted).toBe(true); expect(B.loaders.fetch.signal.aborted).toBe(false); // No-op since the original call was aborted await A.loaders.fetch.resolve("A"); - expect(t.router.state.fetchers.get(key)?.state).toBe("loading"); + expect(t.router.getFetcher(key)?.state).toBe("loading"); // Complete the navigation await B.loaders.fetch.resolve("B"); expect(t.router.state.navigation.state).toBe("idle"); - expect(t.router.state.fetchers.get(key)).toMatchObject({ + expect(t.fetchers[key]).toMatchObject({ state: "idle", data: "B", }); @@ -2738,19 +2769,19 @@ describe("fetchers", () => { let key = "key"; let A = await t.fetch("/fetch", key, "root"); - expect(t.router.state.fetchers.get(key)?.state).toBe("loading"); + expect(t.router.getFetcher(key)?.state).toBe("loading"); let B = await t.navigate("/page"); - expect(t.router.state.fetchers.get(key)?.state).toBe("loading"); + expect(t.router.getFetcher(key)?.state).toBe("loading"); expect(A.loaders.fetch.signal.aborted).toBe(false); await A.loaders.fetch.resolve("A"); - expect(t.router.state.fetchers.get(key)?.state).toBe("idle"); + expect(t.router.getFetcher(key)?.state).toBe("idle"); // Complete the navigation await B.loaders.page.resolve("PAGE"); expect(t.router.state.navigation.state).toBe("idle"); - expect(t.router.state.fetchers.get(key)).toMatchObject({ + expect(t.fetchers[key]).toMatchObject({ state: "idle", data: "A", }); @@ -2790,11 +2821,11 @@ describe("fetchers", () => { // fetcher.load() let A = await t.fetch("/parent", key); await A.loaders.parent.resolve("PARENT LOADER"); - expect(t.router.getFetcher(key).data).toBe("PARENT LOADER"); + expect(t.fetchers[key].data).toBe("PARENT LOADER"); let B = await t.fetch("/parent?index", key); await B.loaders.index.resolve("INDEX LOADER"); - expect(t.router.getFetcher(key).data).toBe("INDEX LOADER"); + expect(t.fetchers[key].data).toBe("INDEX LOADER"); // fetcher.submit({}, { method: 'get' }) let C = await t.fetch("/parent", key, { @@ -2802,14 +2833,14 @@ describe("fetchers", () => { formData: createFormData({}), }); await C.loaders.parent.resolve("PARENT LOADER"); - expect(t.router.getFetcher(key).data).toBe("PARENT LOADER"); + expect(t.fetchers[key].data).toBe("PARENT LOADER"); let D = await t.fetch("/parent?index", key, { formMethod: "get", formData: createFormData({}), }); await D.loaders.index.resolve("INDEX LOADER"); - expect(t.router.getFetcher(key).data).toBe("INDEX LOADER"); + expect(t.fetchers[key].data).toBe("INDEX LOADER"); // fetcher.submit({}, { method: 'post' }) let E = await t.fetch("/parent", key, { @@ -2817,14 +2848,14 @@ describe("fetchers", () => { formData: createFormData({}), }); await E.actions.parent.resolve("PARENT ACTION"); - expect(t.router.getFetcher(key).data).toBe("PARENT ACTION"); + expect(t.fetchers[key].data).toBe("PARENT ACTION"); let F = await t.fetch("/parent?index", key, { formMethod: "post", formData: createFormData({}), }); await F.actions.index.resolve("INDEX ACTION"); - expect(t.router.getFetcher(key).data).toBe("INDEX ACTION"); + expect(t.fetchers[key].data).toBe("INDEX ACTION"); }); it("throws a 404 ErrorResponse without ?index and parent route has no loader", async () => { @@ -2986,7 +3017,7 @@ describe("fetchers", () => { formEncType: "application/x-www-form-urlencoded", body, }); - expect(t.router.state.fetchers.get("key")?.formData?.get("a")).toBe("1"); + expect(t.router.getFetcher("key")?.formData?.get("a")).toBe("1"); await F.actions.root.resolve("ACTION"); @@ -3015,7 +3046,7 @@ describe("fetchers", () => { formEncType: "application/json", body, }); - expect(t.router.state.fetchers.get("key")?.json).toBe(body); + expect(t.router.getFetcher("key")?.json).toBe(body); await F.actions.root.resolve("ACTION"); expect(F.actions.root.stub).toHaveBeenCalledWith({ @@ -3041,7 +3072,7 @@ describe("fetchers", () => { formEncType: "application/json", body, }); - expect(t.router.state.fetchers.get("key")?.json).toBe(body); + expect(t.router.getFetcher("key")?.json).toBe(body); await F.actions.root.resolve("ACTION"); expect(F.actions.root.stub).toHaveBeenCalledWith({ @@ -3067,7 +3098,7 @@ describe("fetchers", () => { formEncType: "application/json", body, }); - expect(t.router.state.fetchers.get("key")?.json).toBe(body); + expect(t.router.getFetcher("key")?.json).toBe(body); await F.actions.root.resolve("ACTION"); expect(F.actions.root.stub).toHaveBeenCalledWith({ @@ -3093,7 +3124,7 @@ describe("fetchers", () => { formEncType: "text/plain", body, }); - expect(t.router.state.fetchers.get("key")?.text).toBe(body); + expect(t.router.getFetcher("key")?.text).toBe(body); await F.actions.root.resolve("ACTION"); @@ -3122,7 +3153,7 @@ describe("fetchers", () => { formEncType: "text/plain", body, }); - expect(t.router.state.fetchers.get("key")?.text).toBe(body); + expect(t.router.getFetcher("key")?.text).toBe(body); await F.actions.root.resolve("ACTION"); @@ -3150,7 +3181,7 @@ describe("fetchers", () => { formMethod: "post", body, }); - expect(t.router.state.fetchers.get("key")?.formData?.get("a")).toBe("1"); + expect(t.router.getFetcher("key")?.formData?.get("a")).toBe("1"); await F.actions.root.resolve("ACTION"); diff --git a/packages/react-router/__tests__/router/flush-sync-test.ts b/packages/react-router/__tests__/router/flush-sync-test.ts index 67b7bdead7..6e10cf074b 100644 --- a/packages/react-router/__tests__/router/flush-sync-test.ts +++ b/packages/react-router/__tests__/router/flush-sync-test.ts @@ -99,20 +99,20 @@ describe("flushSync", () => { expect.anything(), expect.objectContaining({ unstable_flushSync: false }) ); - expect(t.router.state.fetchers.get(key)?.state).toBe("loading"); + expect(t.fetchers[key].state).toBe("loading"); await A.loaders.root.resolve("ROOT"); - expect(t.router.state.fetchers.get(key)?.data).toBe("ROOT"); + expect(t.fetchers[key].data).toBe("ROOT"); let B = await t.fetch("/", key, { unstable_flushSync: true }); expect(spy).toHaveBeenLastCalledWith( expect.anything(), expect.objectContaining({ unstable_flushSync: true }) ); - expect(t.router.state.fetchers.get(key)?.state).toBe("loading"); + expect(t.fetchers[key].state).toBe("loading"); await B.loaders.root.resolve("ROOT2"); - expect(t.router.state.fetchers.get(key)?.data).toBe("ROOT2"); + expect(t.fetchers[key].data).toBe("ROOT2"); expect(spy).toHaveBeenLastCalledWith( expect.anything(), expect.objectContaining({ unstable_flushSync: false }) @@ -138,14 +138,14 @@ describe("flushSync", () => { expect.anything(), expect.objectContaining({ unstable_flushSync: false }) ); - expect(t.router.state.fetchers.get(key)?.state).toBe("submitting"); + expect(t.fetchers[key].state).toBe("submitting"); await A.actions.root.resolve("ROOT"); expect(spy).toHaveBeenLastCalledWith( expect.anything(), expect.objectContaining({ unstable_flushSync: false }) ); - expect(t.router.state.fetchers.get(key)?.data).toBe("ROOT"); + expect(t.fetchers[key].data).toBe("ROOT"); let B = await t.fetch("/", key, { formMethod: "post", @@ -156,10 +156,10 @@ describe("flushSync", () => { expect.anything(), expect.objectContaining({ unstable_flushSync: true }) ); - expect(t.router.state.fetchers.get(key)?.state).toBe("submitting"); + expect(t.fetchers[key].state).toBe("submitting"); await B.actions.root.resolve("ROOT2"); - expect(t.router.state.fetchers.get(key)?.data).toBe("ROOT2"); + expect(t.fetchers[key].data).toBe("ROOT2"); expect(spy).toHaveBeenLastCalledWith( expect.anything(), expect.objectContaining({ unstable_flushSync: false }) diff --git a/packages/react-router/__tests__/router/lazy-discovery-test.ts b/packages/react-router/__tests__/router/lazy-discovery-test.ts index cf17ac9ca1..02d226a851 100644 --- a/packages/react-router/__tests__/router/lazy-discovery-test.ts +++ b/packages/react-router/__tests__/router/lazy-discovery-test.ts @@ -1,6 +1,7 @@ import type { AgnosticDataRouteObject, Router } from "../../lib/router/index"; import { createMemoryHistory, createRouter } from "../../lib/router/index"; import { ErrorResponseImpl } from "../../lib/router/utils"; +import { getFetcherData } from "./utils/data-router-setup"; import { createDeferred, createFormData, tick } from "./utils/utils"; let router: Router; @@ -1522,6 +1523,7 @@ describe("Lazy Route Discovery (Fog of War)", () => { patch("parent", children); }, }); + let fetcherData = getFetcherData(router); let key = "key"; router.fetch(key, "0", "/parent/child"); @@ -1540,7 +1542,7 @@ describe("Lazy Route Discovery (Fog of War)", () => { await tick(); expect(router.getFetcher(key).state).toBe("idle"); - expect(router.getFetcher(key).data).toBe("CHILD"); + expect(fetcherData.get(key)).toBe("CHILD"); }); it("discovers child routes at a depth >1 (fetcher.load)", async () => { @@ -1578,13 +1580,14 @@ describe("Lazy Route Discovery (Fog of War)", () => { } }, }); + let fetcherData = getFetcherData(router); let key = "key"; await router.fetch(key, "0", "/a/b/c"); // Needed for now since router.fetch is not async until v7 await new Promise((r) => setTimeout(r, 10)); expect(router.getFetcher(key).state).toBe("idle"); - expect(router.getFetcher(key).data).toBe("C"); + expect(fetcherData.get(key)).toBe("C"); }); it("discovers child route at a depth of 1 (fetcher.submit)", async () => { @@ -1607,6 +1610,7 @@ describe("Lazy Route Discovery (Fog of War)", () => { patch("parent", children); }, }); + let fetcherData = getFetcherData(router); let key = "key"; router.fetch(key, "0", "/parent/child", { @@ -1628,7 +1632,7 @@ describe("Lazy Route Discovery (Fog of War)", () => { await tick(); expect(router.getFetcher(key).state).toBe("idle"); - expect(router.getFetcher(key).data).toBe("CHILD"); + expect(fetcherData.get(key)).toBe("CHILD"); }); it("discovers child routes at a depth >1 (fetcher.submit)", async () => { @@ -1666,6 +1670,7 @@ describe("Lazy Route Discovery (Fog of War)", () => { } }, }); + let fetcherData = getFetcherData(router); let key = "key"; await router.fetch(key, "0", "/a/b/c", { @@ -1675,7 +1680,7 @@ describe("Lazy Route Discovery (Fog of War)", () => { // Needed for now since router.fetch is not async until v7 await new Promise((r) => setTimeout(r, 10)); expect(router.getFetcher(key).state).toBe("idle"); - expect(router.getFetcher(key).data).toBe("C ACTION"); + expect(fetcherData.get(key)).toBe("C ACTION"); }); }); }); diff --git a/packages/react-router/__tests__/router/lazy-test.ts b/packages/react-router/__tests__/router/lazy-test.ts index 6ce0c57b5e..f4b5157fba 100644 --- a/packages/react-router/__tests__/router/lazy-test.ts +++ b/packages/react-router/__tests__/router/lazy-test.ts @@ -101,43 +101,6 @@ describe("lazily loaded route modules", () => { }); expect(router.state.matches[0].route).toMatchObject(route); }); - - it("fetches lazy route modules and executes loaders with v7_partialHydration enabled", async () => { - let dfd = createDeferred(); - let router = createRouter({ - routes: [ - { - path: "/lazy", - lazy: () => dfd.promise, - }, - ], - history: createMemoryHistory({ initialEntries: ["/lazy"] }), - future: { - v7_partialHydration: true, - }, - }); - - expect(router.state.initialized).toBe(false); - - router.initialize(); - - let loaderDfd = createDeferred(); - let route = { - Component: () => null, - loader: () => loaderDfd.promise, - }; - await dfd.resolve(route); - expect(router.state.initialized).toBe(false); - - await loaderDfd.resolve("LOADER"); - expect(router.state.location.pathname).toBe("/lazy"); - expect(router.state.navigation.state).toBe("idle"); - expect(router.state.initialized).toBe(true); - expect(router.state.loaderData).toEqual({ - "0": "LOADER", - }); - expect(router.state.matches[0].route).toMatchObject(route); - }); }); describe("happy path", () => { @@ -216,8 +179,8 @@ describe("lazily loaded route modules", () => { expect(t.router.state.fetchers.get(key)?.state).toBe("loading"); await loaderDfd.resolve("LAZY LOADER"); - expect(t.router.state.fetchers.get(key)?.state).toBe("idle"); - expect(t.router.state.fetchers.get(key)?.data).toBe("LAZY LOADER"); + expect(t.fetchers[key].state).toBe("idle"); + expect(t.fetchers[key].data).toBe("LAZY LOADER"); }); it("fetches lazy route modules on fetcher.submit", async () => { @@ -237,8 +200,8 @@ describe("lazily loaded route modules", () => { expect(t.router.state.fetchers.get(key)?.state).toBe("submitting"); await actionDfd.resolve("LAZY ACTION"); - expect(t.router.state.fetchers.get(key)?.state).toBe("idle"); - expect(t.router.state.fetchers.get(key)?.data).toBe("LAZY ACTION"); + expect(t.fetchers[key]?.state).toBe("idle"); + expect(t.fetchers[key]?.data).toBe("LAZY ACTION"); }); it("fetches lazy route modules on staticHandler.query()", async () => { @@ -671,8 +634,8 @@ describe("lazily loaded route modules", () => { await loaderDfdA.resolve("LAZY LOADER A"); await loaderDfdB.resolve("LAZY LOADER B"); - expect(t.router.state.fetchers.get(key)?.state).toBe("idle"); - expect(t.router.state.fetchers.get(key)?.data).toBe("LAZY LOADER B"); + expect(t.fetchers[key].state).toBe("idle"); + expect(t.fetchers[key].data).toBe("LAZY LOADER B"); expect(lazyLoaderStubA).not.toHaveBeenCalled(); expect(lazyloaderStubB).toHaveBeenCalledTimes(2); }); @@ -712,8 +675,8 @@ describe("lazily loaded route modules", () => { await actionDfdA.resolve("LAZY ACTION A"); await actionDfdB.resolve("LAZY ACTION B"); - expect(t.router.state.fetchers.get(key)?.state).toBe("idle"); - expect(t.router.state.fetchers.get(key)?.data).toBe("LAZY ACTION B"); + expect(t.fetchers[key].state).toBe("idle"); + expect(t.fetchers[key].data).toBe("LAZY ACTION B"); expect(lazyActionStubA).not.toHaveBeenCalled(); expect(lazyActionStubB).toHaveBeenCalledTimes(2); }); @@ -832,13 +795,13 @@ describe("lazily loaded route modules", () => { await A.lazy.lazy.resolve({ loader: lazyLoaderStubA, }); - expect(t.router.state.fetchers.get(key)?.state).toBe("loading"); + expect(t.fetchers[key].state).toBe("loading"); await loaderDfdA.resolve("LAZY LOADER A"); await loaderDfdB.resolve("LAZY LOADER B"); - expect(t.router.state.fetchers.get(key)?.state).toBe("idle"); - expect(t.router.state.fetchers.get(key)?.data).toBe("LAZY LOADER B"); + expect(t.fetchers[key].state).toBe("idle"); + expect(t.fetchers[key].data).toBe("LAZY LOADER B"); expect(lazyLoaderStubA).not.toHaveBeenCalled(); expect(lazyLoaderStubB).toHaveBeenCalledTimes(2); }); diff --git a/packages/react-router/__tests__/router/redirects-test.ts b/packages/react-router/__tests__/router/redirects-test.ts index 3c1f39c8e4..0e0c4de407 100644 --- a/packages/react-router/__tests__/router/redirects-test.ts +++ b/packages/react-router/__tests__/router/redirects-test.ts @@ -156,10 +156,8 @@ describe("redirects", () => { loaderData: {}, errors: null, }); - expect(t.router.state.fetchers.get("key")).toMatchObject({ - state: "idle", - data: undefined, - }); + // There's never any data so it just ends up being removed from state.fetchers + expect(t.fetchers["key"]).toBeUndefined(); }); it("supports relative routing in redirects (from child fetch loader)", async () => { diff --git a/packages/react-router/__tests__/router/revalidate-test.ts b/packages/react-router/__tests__/router/revalidate-test.ts index 3255fd4397..f54e535fbe 100644 --- a/packages/react-router/__tests__/router/revalidate-test.ts +++ b/packages/react-router/__tests__/router/revalidate-test.ts @@ -904,7 +904,7 @@ describe("router.revalidate", () => { let key = "key"; let F = await t.fetch("/", key); await F.loaders.root.resolve("ROOT_DATA*"); - expect(t.router.state.fetchers.get(key)).toMatchObject({ + expect(t.fetchers[key]).toMatchObject({ state: "idle", data: "ROOT_DATA*", }); @@ -912,7 +912,7 @@ describe("router.revalidate", () => { let R = await t.revalidate(); await R.loaders.root.resolve("ROOT_DATA**"); await R.loaders.index.resolve("INDEX_DATA"); - expect(t.router.state.fetchers.get(key)).toMatchObject({ + expect(t.fetchers[key]).toMatchObject({ state: "idle", data: "ROOT_DATA**", }); diff --git a/packages/react-router/__tests__/router/router-test.ts b/packages/react-router/__tests__/router/router-test.ts index a05fc2afa8..3722a15488 100644 --- a/packages/react-router/__tests__/router/router-test.ts +++ b/packages/react-router/__tests__/router/router-test.ts @@ -18,6 +18,7 @@ import { import { cleanup, createDeferred, + getFetcherData, setup, TASK_ROUTES, } from "./utils/data-router-setup"; @@ -2300,12 +2301,14 @@ describe("a router", () => { }, }, }); + let fetcherData = getFetcherData(router); router.initialize(); let key = "key"; router.fetch(key, "root", "/foo"); await fooDfd.resolve("FOO"); - expect(router.state.fetchers.get("key")?.data).toBe("FOO"); + await tick(); + expect(fetcherData.get(key)).toBe("FOO"); let rootDfd2 = createDeferred(); let newRoutes: AgnosticDataRouteObject[] = [ @@ -2342,6 +2345,7 @@ describe("a router", () => { // Resolve any loaders that should have ran (foo's loader has been removed) await rootDfd2.resolve("ROOT*"); + await tick(); expect(router.state.revalidation).toBe("idle"); // Routes should be updated @@ -2352,9 +2356,12 @@ describe("a router", () => { expect(router.state.loaderData).toEqual({ root: "ROOT*", }); + // Fetcher should have been revalidated but throw an error since the // loader was removed - expect(router.state.fetchers.get("key")?.data).toBe(undefined); + // The data remains in the UI layer in this test setup since it hasn't + // unmounted - but normally it would unmount and the data would be removed + expect(fetcherData.get("key")).toBe("FOO"); expect(router.state.errors).toMatchInlineSnapshot(` { "root": ErrorResponseImpl { @@ -2404,12 +2411,13 @@ describe("a router", () => { }, }, }); + let fetcherData = getFetcherData(router); router.initialize(); let key = "key"; router.fetch(key, "root", "/foo"); await fooDfd.resolve("FOO"); - expect(router.state.fetchers.get("key")?.data).toBe("FOO"); + expect(fetcherData.get(key)).toBe("FOO"); let rootDfd2 = createDeferred(); let newRoutes: AgnosticDataRouteObject[] = [ @@ -2450,8 +2458,10 @@ describe("a router", () => { expect(router.state.loaderData).toEqual({ root: "ROOT*", }); - // Fetcher should have been revalidated but theown a 404 wince the route was removed - expect(router.state.fetchers.get("key")?.data).toBe(undefined); + // Fetcher should have been revalidated but thrown a 404 wince the route was removed + // The data remains in the UI layer in this test setup since it hasn't + // unmounted - but normally it would unmount and the data would be removed + expect(fetcherData.get(key)).toBe("FOO"); expect(router.state.errors).toEqual({ root: new ErrorResponseImpl( 404, diff --git a/packages/react-router/__tests__/router/should-revalidate-test.ts b/packages/react-router/__tests__/router/should-revalidate-test.ts index 69037af7ed..ee27a5659f 100644 --- a/packages/react-router/__tests__/router/should-revalidate-test.ts +++ b/packages/react-router/__tests__/router/should-revalidate-test.ts @@ -2,7 +2,7 @@ import { createMemoryHistory, createRouter, redirect } from "../../lib/router"; import type { ShouldRevalidateFunctionArgs } from "../../lib/router"; import { UNSAFE_ErrorResponseImpl as ErrorResponseImpl } from "../../lib/router"; import { urlMatch } from "./utils/custom-matchers"; -import { cleanup } from "./utils/data-router-setup"; +import { cleanup, getFetcherData } from "./utils/data-router-setup"; import { createFormData, tick } from "./utils/utils"; interface CustomMatchers { @@ -593,16 +593,15 @@ describe("shouldRevalidate", () => { }, ], }); + let fetcherData = getFetcherData(router); router.initialize(); await tick(); let key = "key"; router.fetch(key, "root", "/fetch"); await tick(); - expect(router.state.fetchers.get(key)).toMatchObject({ - state: "idle", - data: "FETCH 1", - }); + expect(router.getFetcher(key).state).toBe("idle"); + expect(fetcherData.get(key)).toBe("FETCH 1"); expect(shouldRevalidate.mock.calls.length).toBe(0); // Normal navigations should trigger fetcher shouldRevalidate with @@ -617,10 +616,8 @@ describe("shouldRevalidate", () => { nextUrl: expect.urlMatch("http://localhost/child"), defaultShouldRevalidate: false, }); - expect(router.state.fetchers.get(key)).toMatchObject({ - state: "idle", - data: "FETCH 1", - }); + expect(router.getFetcher(key).state).toBe("idle"); + expect(fetcherData.get(key)).toBe("FETCH 1"); router.navigate("/"); await tick(); @@ -632,10 +629,8 @@ describe("shouldRevalidate", () => { nextUrl: expect.urlMatch("http://localhost/"), defaultShouldRevalidate: false, }); - expect(router.state.fetchers.get(key)).toMatchObject({ - state: "idle", - data: "FETCH 1", - }); + expect(router.getFetcher(key).state).toBe("idle"); + expect(fetcherData.get(key)).toBe("FETCH 1"); // Submission navigations should trigger fetcher shouldRevalidate with // defaultShouldRevalidate=true @@ -644,10 +639,8 @@ describe("shouldRevalidate", () => { formData: createFormData({}), }); await tick(); - expect(router.state.fetchers.get(key)).toMatchObject({ - state: "idle", - data: "FETCH 1", - }); + expect(router.getFetcher(key).state).toBe("idle"); + expect(fetcherData.get(key)).toBe("FETCH 1"); expect(shouldRevalidate.mock.calls.length).toBe(3); expect(shouldRevalidate.mock.calls[2][0]).toMatchObject({ currentParams: {}, @@ -691,6 +684,7 @@ describe("shouldRevalidate", () => { }, ], }); + let fetcherData = getFetcherData(router); router.initialize(); await tick(); @@ -700,10 +694,8 @@ describe("shouldRevalidate", () => { formData: createFormData({ key: "value" }), }); await tick(); - expect(router.state.fetchers.get(key)).toMatchObject({ - state: "idle", - data: "FETCH", - }); + expect(router.getFetcher(key).state).toBe("idle"); + expect(fetcherData.get(key)).toBe("FETCH"); let arg = shouldRevalidate.mock.calls[0][0]; expect(arg).toMatchInlineSnapshot(` @@ -755,6 +747,7 @@ describe("shouldRevalidate", () => { }, ], }); + let fetcherData = getFetcherData(router); router.initialize(); await tick(); @@ -764,10 +757,8 @@ describe("shouldRevalidate", () => { formData: createFormData({ key: "value" }), }); await tick(); - expect(router.state.fetchers.get(key)).toMatchObject({ - state: "idle", - data: undefined, - }); + expect(router.getFetcher(key).state).toBe("idle"); + expect(fetcherData.get(key)).toBe(undefined); let arg = shouldRevalidate.mock.calls[0][0]; expect(arg).toMatchInlineSnapshot(` @@ -871,6 +862,7 @@ describe("shouldRevalidate", () => { }, }, }); + let fetcherData = getFetcherData(router); router.initialize(); await tick(); @@ -881,10 +873,8 @@ describe("shouldRevalidate", () => { formData: createFormData({ key: "value" }), }); await tick(); - expect(router.state.fetchers.get(key)).toMatchObject({ - state: "idle", - data: "FETCH 1", - }); + expect(router.getFetcher(key).state).toBe("idle"); + expect(fetcherData.get(key)).toBe("FETCH 1"); expect(router.state.loaderData).toMatchObject({ index: "INDEX", }); @@ -894,10 +884,8 @@ describe("shouldRevalidate", () => { formData: createFormData({ key: "value" }), }); await tick(); - expect(router.state.fetchers.get(key)).toMatchObject({ - state: "idle", - data: "FETCH 2", - }); + expect(router.getFetcher(key).state).toBe("idle"); + expect(fetcherData.get(key)).toBe("FETCH 2"); expect(router.state.loaderData).toMatchObject({ index: "INDEX", }); diff --git a/packages/react-router/__tests__/router/utils/data-router-setup.ts b/packages/react-router/__tests__/router/utils/data-router-setup.ts index faa51850a8..a9cdce13bd 100644 --- a/packages/react-router/__tests__/router/utils/data-router-setup.ts +++ b/packages/react-router/__tests__/router/utils/data-router-setup.ts @@ -14,6 +14,7 @@ import { matchRoutes, redirect, parsePath, + IDLE_FETCHER, } from "../../../lib/router"; // Private API @@ -169,13 +170,33 @@ export function createDeferred() { }; } +export function getFetcherData(router: Router) { + let fetcherData = new Map(); + router.subscribe((state, { deletedFetchers }) => { + deletedFetchers.forEach((k) => { + fetcherData.delete(k); + }); + state.fetchers.forEach((fetcher, key) => { + if ( + fetcher.data !== undefined && + // action fetch + ((fetcher.formMethod !== "GET" && fetcher.state === "loading") || + // normal fetch + fetcher.state === "idle") + ) { + fetcherData.set(key, fetcher.data); + } + }); + }); + return fetcherData; +} + export function setup({ routes, basename, initialEntries, initialIndex, hydrationData, - future, dataStrategy, }: SetupOpts) { let guid = 0; @@ -316,10 +337,12 @@ export function setup({ history, routes: enhanceRoutes(routes), hydrationData, - future, window: testWindow, unstable_dataStrategy: dataStrategy, - }).initialize(); + }); + + let fetcherData = getFetcherData(currentRouter); + currentRouter.initialize(); function getRouteHelpers( routeId: string, @@ -545,7 +568,10 @@ export function setup({ navigationId, get fetcher() { invariant(currentRouter, "No currentRouter available"); - return currentRouter.getFetcher(key); + return { + ...currentRouter.getFetcher(key), + data: fetcherData.get(key), + }; }, lazy: lazyHelpers, loaders: loaderHelpers, @@ -687,7 +713,7 @@ export function setup({ // start a new navigation so don't increment here navigationId = currentRouter.state.navigation.state === "submitting" && - currentRouter.state.navigation.formMethod !== "get" + currentRouter.state.navigation.formMethod !== "GET" ? guid : ++guid; activeLoaderType = "navigation"; @@ -720,6 +746,24 @@ export function setup({ window: testWindow, history, router: currentRouter, + get fetchers() { + let fetchers = {}; + currentRouter?.state.fetchers.forEach((f, key) => { + fetchers[key] = { + ...f, + data: fetcherData.get(key), + }; + }); + fetcherData.forEach((data, key) => { + if (!fetchers[key]) { + fetchers[key] = { + ...IDLE_FETCHER, + data: fetcherData.get(key), + }; + } + }); + return fetchers; + }, navigate, fetch, revalidate, diff --git a/packages/react-router/__tests__/useResolvedPath-test.tsx b/packages/react-router/__tests__/useResolvedPath-test.tsx index e6a8b28227..7f183b4f7a 100644 --- a/packages/react-router/__tests__/useResolvedPath-test.tsx +++ b/packages/react-router/__tests__/useResolvedPath-test.tsx @@ -368,10 +368,7 @@ describe("useResolvedPath", () => { // gh-issue #11629 it("'.' resolves to the current path including any splat paths nested in pathless routes", () => { let { container } = render( - + diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx index e0649a29b2..0e9830c581 100644 --- a/packages/react-router/lib/components.tsx +++ b/packages/react-router/lib/components.tsx @@ -1,5 +1,5 @@ import type { - FutureConfig as RouterFutureConfig, + FutureConfig, HydrationState, InitialEntry, LazyRouteFunction, @@ -127,7 +127,7 @@ export function createMemoryRouter( routes: RouteObject[], opts?: { basename?: string; - future?: Partial; + future?: Partial; hydrationData?: HydrationState; initialEntries?: InitialEntry[]; initialIndex?: number; diff --git a/packages/react-router/lib/dom/lib.tsx b/packages/react-router/lib/dom/lib.tsx index 2e4bf5a7ed..a9fb38a42c 100644 --- a/packages/react-router/lib/dom/lib.tsx +++ b/packages/react-router/lib/dom/lib.tsx @@ -12,7 +12,7 @@ import type { unstable_DataStrategyFunction, Fetcher, FormEncType, - FutureConfig as RouterFutureConfig, + FutureConfig, GetScrollRestorationKeyFunction, HashHistory, History, @@ -128,7 +128,7 @@ try { interface DOMRouterOpts { basename?: string; - future?: Partial; + future?: Partial; hydrationData?: HydrationState; unstable_dataStrategy?: unstable_DataStrategyFunction; unstable_patchRoutesOnMiss?: unstable_PatchRoutesOnMissFunction; @@ -2152,12 +2152,7 @@ export function useFetcher({ // Registration/cleanup React.useEffect(() => { router.getFetcher(fetcherKey); - return () => { - // Tell the router we've unmounted - if v7_fetcherPersist is enabled this - // will not delete immediately but instead queue up a delete after the - // fetcher returns to an `idle` state - router.deleteFetcher(fetcherKey); - }; + return () => router.deleteFetcher(fetcherKey); }, [router, fetcherKey]); // Fetcher additions diff --git a/packages/react-router/lib/dom/server.tsx b/packages/react-router/lib/dom/server.tsx index 8e41bff049..000f951d91 100644 --- a/packages/react-router/lib/dom/server.tsx +++ b/packages/react-router/lib/dom/server.tsx @@ -8,7 +8,7 @@ import type { CreateStaticHandlerOptions as RouterCreateStaticHandlerOptions, UNSAFE_RouteManifest as RouteManifest, RouterState, - FutureConfig as RouterFutureConfig, + FutureConfig, To, } from "../router"; import { @@ -270,7 +270,9 @@ export function createStaticHandler( export function createStaticRouter( routes: RouteObject[], context: StaticHandlerContext, - opts: {} = {} + opts: { + future?: Partial; + } = {} ): RemixRouter { let manifest: RouteManifest = {}; let dataRoutes = convertRoutesToDataRoutes( @@ -300,8 +302,8 @@ export function createStaticRouter( }, get future() { return { - v7_fetcherPersist: false, unstable_skipActionErrorRevalidation: false, + ...opts?.future, }; }, get state() { diff --git a/packages/react-router/lib/dom/ssr/browser.tsx b/packages/react-router/lib/dom/ssr/browser.tsx index 8c0c8f811e..606ab1d810 100644 --- a/packages/react-router/lib/dom/ssr/browser.tsx +++ b/packages/react-router/lib/dom/ssr/browser.tsx @@ -188,7 +188,6 @@ function createHydratedRouter(): RemixRouter { history: createBrowserHistory(), basename: ssrInfo.context.basename, future: { - v7_fetcherPersist: ssrInfo.context.future.v3_fetcherPersist, // Single fetch enables this underlying behavior unstable_skipActionErrorRevalidation: true, }, diff --git a/packages/react-router/lib/router/router.ts b/packages/react-router/lib/router/router.ts index 01c4c09244..974e1d9b93 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< * Future flags to toggle new feature behavior */ export interface FutureConfig { - v7_fetcherPersist: boolean; unstable_skipActionErrorRevalidation: boolean; } @@ -840,7 +839,6 @@ export function createRouter(init: RouterInit): Router { // Config driven behavior flags let future: FutureConfig = { - v7_fetcherPersist: false, unstable_skipActionErrorRevalidation: false, ...init.future, }; @@ -1006,8 +1004,8 @@ export function createRouter(init: RouterInit): Router { // Ref-count mounted fetchers so we know when it's ok to clean them up let activeFetchers = new Map(); - // Fetchers that have requested a delete when using v7_fetcherPersist, - // they'll be officially removed after they return to idle + // Fetchers that have requested a delete - they'll be officially removed after + // they return to idle let deletedFetchers = new Set(); // Store DeferredData instances for active route matches. When a @@ -1160,20 +1158,18 @@ export function createRouter(init: RouterInit): Router { let completedFetchers: string[] = []; let deletedFetchersKeys: string[] = []; - if (future.v7_fetcherPersist) { - state.fetchers.forEach((fetcher, key) => { - if (fetcher.state === "idle") { - if (deletedFetchers.has(key)) { - // Unmounted from the UI and can be totally removed - deletedFetchersKeys.push(key); - } else { - // Returned to idle but still mounted in the UI, so semi-remains for - // revalidations and such - completedFetchers.push(key); - } + state.fetchers.forEach((fetcher, key) => { + if (fetcher.state === "idle") { + if (deletedFetchers.has(key)) { + // Unmounted from the UI and can be totally removed + deletedFetchersKeys.push(key); + } else { + // Returned to idle but still mounted in the UI, so semi-remains for + // revalidations and such + completedFetchers.push(key); } - }); - } + } + }); // Iterate over a local copy so that if flushSync is used and we end up // removing and adding a new subscriber due to the useCallback dependencies, @@ -1187,10 +1183,8 @@ export function createRouter(init: RouterInit): Router { ); // Remove idle fetchers from state since we only care about in-flight fetchers. - if (future.v7_fetcherPersist) { - completedFetchers.forEach((key) => state.fetchers.delete(key)); - deletedFetchersKeys.forEach((key) => deleteFetcher(key)); - } + completedFetchers.forEach((key) => state.fetchers.delete(key)); + deletedFetchersKeys.forEach((key) => deleteFetcher(key)); } // Complete a navigation returning the state.navigation back to the IDLE_NAVIGATION @@ -2307,10 +2301,9 @@ export function createRouter(init: RouterInit): Router { return; } - // When using v7_fetcherPersist, we don't want errors bubbling up to the UI - // or redirects processed for unmounted fetchers so we just revert them to - // idle - if (future.v7_fetcherPersist && deletedFetchers.has(key)) { + // We don't want errors bubbling up to the UI or redirects processed for + // unmounted fetchers so we just revert them to idle + if (deletedFetchers.has(key)) { if (isRedirectResult(actionResult) || isErrorResult(actionResult)) { updateFetcherState(key, getDoneFetcher(undefined)); return; @@ -2910,13 +2903,11 @@ export function createRouter(init: RouterInit): Router { } function getFetcher(key: string): Fetcher { - if (future.v7_fetcherPersist) { - activeFetchers.set(key, (activeFetchers.get(key) || 0) + 1); - // If this fetcher was previously marked for deletion, unmark it since we - // have a new instance - if (deletedFetchers.has(key)) { - deletedFetchers.delete(key); - } + activeFetchers.set(key, (activeFetchers.get(key) || 0) + 1); + // If this fetcher was previously marked for deletion, unmark it since we + // have a new instance + if (deletedFetchers.has(key)) { + deletedFetchers.delete(key); } return state.fetchers.get(key) || IDLE_FETCHER; } @@ -2940,16 +2931,12 @@ export function createRouter(init: RouterInit): Router { } function deleteFetcherAndUpdateState(key: string): void { - if (future.v7_fetcherPersist) { - let count = (activeFetchers.get(key) || 0) - 1; - if (count <= 0) { - activeFetchers.delete(key); - deletedFetchers.add(key); - } else { - activeFetchers.set(key, count); - } + let count = (activeFetchers.get(key) || 0) - 1; + if (count <= 0) { + activeFetchers.delete(key); + deletedFetchers.add(key); } else { - deleteFetcher(key); + activeFetchers.set(key, count); } updateState({ fetchers: new Map(state.fetchers) }); } @@ -4410,9 +4397,8 @@ function getMatchesToLoad( fetchLoadMatches.forEach((f, key) => { // Don't revalidate: // - on initial load (shouldn't be any fetchers then anyway) - // - if fetcher won't be present in the subsequent render - // - no longer matches the URL (v7_fetcherPersist=false) - // - was unmounted but persisted due to v7_fetcherPersist=true + // - if fetcher no longer matches the URL + // - if fetcher was unmounted if ( isInitialLoad || !matches.some((m) => m.route.id === f.routeId) ||