diff --git a/.changeset/unmounted-fetcher-revalidate.md b/.changeset/unmounted-fetcher-revalidate.md
new file mode 100644
index 0000000000..feb70e08f8
--- /dev/null
+++ b/.changeset/unmounted-fetcher-revalidate.md
@@ -0,0 +1,5 @@
+---
+"@remix-run/router": patch
+---
+
+Do not revalidate unmounted fetchers when `v7_fetcherPersist` is enabled
diff --git a/packages/react-router-dom/__tests__/data-browser-router-test.tsx b/packages/react-router-dom/__tests__/data-browser-router-test.tsx
index 1c8d57221e..af2f85c214 100644
--- a/packages/react-router-dom/__tests__/data-browser-router-test.tsx
+++ b/packages/react-router-dom/__tests__/data-browser-router-test.tsx
@@ -6288,6 +6288,107 @@ function testDomRouter(
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 (
+ <>
+
+ {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");
+ },
+ },
+ ],
+ { window: getWindow("/"), future: { v7_fetcherPersist: true } }
+ );
+
+ function FetcherComponent({ onClose }) {
+ let fetcher = useFetcher();
+
+ React.useEffect(() => {
+ if (fetcher.state === "idle" && fetcher.data) {
+ onClose(fetcher.data);
+ }
+ }, [fetcher, onClose]);
+
+ return (
+ <>
+
+ {`Fetcher State: ${fetcher.state}`}
+ >
+ );
+ }
+
+ render();
+
+ 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")
+ );
+
+ 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")
+ );
+
+ expect(count).toBe(1);
+ });
});
});
diff --git a/packages/router/router.ts b/packages/router/router.ts
index c14c4814da..21453256af 100644
--- a/packages/router/router.ts
+++ b/packages/router/router.ts
@@ -1632,6 +1632,7 @@ export function createRouter(init: RouterInit): Router {
isRevalidationRequired,
cancelledDeferredRoutes,
cancelledFetcherLoads,
+ deletedFetchers,
fetchLoadMatches,
fetchRedirectIds,
routesToUse,
@@ -2005,6 +2006,7 @@ export function createRouter(init: RouterInit): Router {
isRevalidationRequired,
cancelledDeferredRoutes,
cancelledFetcherLoads,
+ deletedFetchers,
fetchLoadMatches,
fetchRedirectIds,
routesToUse,
@@ -3549,6 +3551,7 @@ function getMatchesToLoad(
isRevalidationRequired: boolean,
cancelledDeferredRoutes: string[],
cancelledFetcherLoads: string[],
+ deletedFetchers: Set,
fetchLoadMatches: Map,
fetchRedirectIds: Set,
routesToUse: AgnosticDataRouteObject[],
@@ -3616,7 +3619,10 @@ function getMatchesToLoad(
let revalidatingFetchers: RevalidatingFetcher[] = [];
fetchLoadMatches.forEach((f, key) => {
// Don't revalidate if fetcher won't be present in the subsequent render
- if (!matches.some((m) => m.route.id === f.routeId)) {
+ if (
+ !matches.some((m) => m.route.id === f.routeId) ||
+ deletedFetchers.has(key)
+ ) {
return;
}