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 (
-