diff --git a/.changeset/expose-promises.md b/.changeset/expose-promises.md
new file mode 100644
index 0000000000..43bf5fbd63
--- /dev/null
+++ b/.changeset/expose-promises.md
@@ -0,0 +1,10 @@
+---
+"react-router": major
+---
+
+- Expose the underlying router promises from the following APIs for compsition in React 19 APIs:
+ - `useNavigate()`
+ - `useSubmit`
+ - `useFetcher().load`
+ - `useFetcher().submit`
+ - `useRevalidator.revalidate`
diff --git a/packages/react-router/__tests__/data-memory-router-test.tsx b/packages/react-router/__tests__/data-memory-router-test.tsx
index e9d4a748d2..f40c085799 100644
--- a/packages/react-router/__tests__/data-memory-router-test.tsx
+++ b/packages/react-router/__tests__/data-memory-router-test.tsx
@@ -29,11 +29,18 @@ import {
useRouteLoaderData,
} from "react-router";
-import type { ErrorResponse } from "../index";
+import {
+ useFetcher,
+ useNavigate,
+ useRevalidator,
+ useSubmit,
+ type ErrorResponse,
+} from "../index";
import urlDataStrategy from "./router/utils/urlDataStrategy";
import { createDeferred } from "./router/utils/utils";
import MemoryNavigate from "./utils/MemoryNavigate";
import getHtml from "./utils/getHtml";
+import { RouterProvider as DomRouterProvider } from "../lib/dom/lib";
describe("createMemoryRouter", () => {
let consoleWarn: jest.SpyInstance;
@@ -1002,6 +1009,272 @@ describe("createMemoryRouter", () => {
`);
});
+ it("exposes promise from useNavigate", async () => {
+ let sequence: string[] = [];
+ let router = createMemoryRouter([
+ {
+ path: "/",
+ Component() {
+ let navigate = useNavigate();
+ return (
+ <>
+
Home
+
+ >
+ );
+ },
+ },
+ {
+ path: "/page",
+ async loader() {
+ sequence.push("loader start");
+ await new Promise((r) => setTimeout(r, 100));
+ sequence.push("loader end");
+ return null;
+ },
+ Component: () => {
+ sequence.push("render");
+ return Page
;
+ },
+ },
+ ]);
+ let { container } = render();
+
+ expect(getHtml(container)).toContain("Home");
+ fireEvent.click(screen.getByText("Navigate"));
+ await waitFor(() => screen.getByText("Page"));
+
+ expect(sequence).toEqual([
+ "call navigate",
+ "loader start",
+ "loader end",
+ "navigate resolved",
+ "render",
+ ]);
+ });
+
+ it("exposes promise from useSubmit", async () => {
+ let sequence: string[] = [];
+ let router = createMemoryRouter([
+ {
+ path: "/",
+ Component() {
+ let submit = useSubmit();
+ return (
+ <>
+ Home
+
+ >
+ );
+ },
+ },
+ {
+ path: "/page",
+ async loader() {
+ sequence.push("loader start");
+ await new Promise((r) => setTimeout(r, 100));
+ sequence.push("loader end");
+ return null;
+ },
+ Component: () => {
+ sequence.push("render");
+ return Page
;
+ },
+ },
+ ]);
+ let { container } = render();
+
+ expect(getHtml(container)).toContain("Home");
+ fireEvent.click(screen.getByText("Submit"));
+ await waitFor(() => screen.getByText("Page"));
+
+ expect(sequence).toEqual([
+ "call submit",
+ "loader start",
+ "loader end",
+ "submit resolved",
+ "render",
+ ]);
+ });
+
+ it("exposes promise from useRevalidator", async () => {
+ let sequence: string[] = [];
+ let count = 0;
+ let router = createMemoryRouter([
+ {
+ path: "/",
+ async loader() {
+ sequence.push("loader start");
+ await new Promise((r) => setTimeout(r, 100));
+ sequence.push("loader end");
+ return ++count;
+ },
+ Component() {
+ let loaderCount = useLoaderData();
+ let revalidator = useRevalidator();
+ sequence.push(`render ${loaderCount}`);
+ return (
+
+ );
+ },
+ },
+ ]);
+ render();
+
+ await waitFor(() => screen.getByText("Revalidate (1)"));
+ fireEvent.click(screen.getByText("Revalidate (1)"));
+ await waitFor(() => screen.getByText("Revalidate (2)"));
+
+ expect(sequence).toEqual([
+ "loader start",
+ "loader end",
+ "render 1",
+ "call revalidate",
+ "loader start",
+ "render 1", // revalidator.state === 'loading'
+ "loader end",
+ "revalidate resolved",
+ "render 2",
+ ]);
+ });
+
+ it("exposes promise from useFetcher.load", async () => {
+ let sequence: string[] = [];
+ let count = 0;
+ let router = createMemoryRouter([
+ {
+ path: "/",
+ async loader() {
+ sequence.push("loader start");
+ await new Promise((r) => setTimeout(r, 100));
+ sequence.push("loader end");
+ return ++count;
+ },
+ Component() {
+ let loaderCount = useLoaderData();
+ let fetcher = useFetcher();
+ sequence.push(`render ${loaderCount} ${fetcher.data || "empty"}`);
+ return (
+
+ );
+ },
+ },
+ ]);
+
+ // TODO: Fetchers only supported in DomRouterProvider at the moment, but
+ // that should be fixed once we align the two
+ render();
+
+ await waitFor(() => screen.getByText("Fetch (1, empty)"));
+ fireEvent.click(screen.getByText("Fetch (1, empty)"));
+ await waitFor(() => screen.getByText("Fetch (1, 2)"));
+
+ expect(sequence).toEqual([
+ "loader start",
+ "loader end",
+ "render 1 empty",
+ "call fetcher.load",
+ "loader start",
+ "render 1 empty", // fetcher.state === 'loading'
+ "loader end",
+ "fetcher.load resolved",
+ "render 1 2",
+ ]);
+ });
+
+ it("exposes promise from useFetcher.submit", async () => {
+ let sequence: string[] = [];
+ let count = 0;
+ let router = createMemoryRouter([
+ {
+ path: "/",
+ async action() {
+ sequence.push("action start");
+ await new Promise((r) => setTimeout(r, 100));
+ sequence.push("action end");
+ return ++count;
+ },
+ async loader() {
+ sequence.push("loader start");
+ await new Promise((r) => setTimeout(r, 100));
+ sequence.push("loader end");
+ return ++count;
+ },
+ Component() {
+ let loaderCount = useLoaderData();
+ let fetcher = useFetcher();
+ sequence.push(`render ${loaderCount} ${fetcher.data || "empty"}`);
+ return (
+
+ );
+ },
+ },
+ ]);
+
+ // TODO: Fetchers only supported in DomRouterProvider at the moment, but
+ // that should be fixed once we align the two
+ render();
+
+ await waitFor(() => screen.getByText("Fetch (1, empty)"));
+ fireEvent.click(screen.getByText("Fetch (1, empty)"));
+ await waitFor(() => screen.getByText("Fetch (3, 2)"));
+
+ expect(sequence).toEqual([
+ "loader start",
+ "loader end",
+ "render 1 empty",
+ "call fetcher.submit",
+ "action start",
+ "render 1 empty", // fetcher.state === 'submitting'
+ "action end",
+ "loader start",
+ "render 1 2", // fetcher.state === 'loading'
+ "loader end",
+ "fetcher.submit resolved",
+ "render 3 2",
+ ]);
+ });
+
describe("errors", () => {
it("renders hydration errors on leaf elements using errorElement", async () => {
let router = createMemoryRouter(
diff --git a/packages/react-router/__tests__/dom/special-characters-test.tsx b/packages/react-router/__tests__/dom/special-characters-test.tsx
index 1e8e7b4882..67a0cc5cc2 100644
--- a/packages/react-router/__tests__/dom/special-characters-test.tsx
+++ b/packages/react-router/__tests__/dom/special-characters-test.tsx
@@ -831,8 +831,9 @@ describe("special character tests", () => {
it("does not encode characters in MemoryRouter (navigate)", () => {
function Start() {
let navigate = useNavigate();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- React.useEffect(() => navigate("/with space"), []);
+ React.useEffect(() => {
+ navigate("/with space");
+ }, [navigate]);
return null;
}
let ctx = render(
@@ -864,8 +865,9 @@ describe("special character tests", () => {
it("does not encode characters in createMemoryRouter (navigate)", () => {
function Start() {
let navigate = useNavigate();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- React.useEffect(() => navigate("/with space"), []);
+ React.useEffect(() => {
+ navigate("/with space");
+ }, [navigate]);
return null;
}
let router = createMemoryRouter([
@@ -903,8 +905,9 @@ describe("special character tests", () => {
function Start() {
let navigate = useNavigate();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- React.useEffect(() => navigate("/with space"), []);
+ React.useEffect(() => {
+ navigate("/with space");
+ }, [navigate]);
return null;
}
@@ -943,8 +946,9 @@ describe("special character tests", () => {
function Start() {
let navigate = useNavigate();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- React.useEffect(() => navigate("/with space"), []);
+ React.useEffect(() => {
+ navigate("/with space");
+ }, [navigate]);
return null;
}
@@ -988,8 +992,9 @@ describe("special character tests", () => {
function Start() {
let navigate = useNavigate();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- React.useEffect(() => navigate("/with space"), []);
+ React.useEffect(() => {
+ navigate("/with space");
+ }, [navigate]);
return null;
}
@@ -1030,8 +1035,9 @@ describe("special character tests", () => {
function Start() {
let navigate = useNavigate();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- React.useEffect(() => navigate("/with space"), []);
+ React.useEffect(() => {
+ navigate("/with space");
+ }, [navigate]);
return null;
}
diff --git a/packages/react-router/__tests__/dom/trailing-slashes-test.tsx b/packages/react-router/__tests__/dom/trailing-slashes-test.tsx
index e54d146633..8628f00f0e 100644
--- a/packages/react-router/__tests__/dom/trailing-slashes-test.tsx
+++ b/packages/react-router/__tests__/dom/trailing-slashes-test.tsx
@@ -634,7 +634,8 @@ function getWindowImpl(initialUrl: string, isHash = false): Window {
function SingleNavigate({ to }: { to: To }) {
let navigate = useNavigate();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- React.useEffect(() => navigate(to), [to]);
+ React.useEffect(() => {
+ navigate(to);
+ }, [navigate, to]);
return null;
}
diff --git a/packages/react-router/__tests__/router/revalidate-test.ts b/packages/react-router/__tests__/router/revalidate-test.ts
index 4ea2e22846..8f15661e36 100644
--- a/packages/react-router/__tests__/router/revalidate-test.ts
+++ b/packages/react-router/__tests__/router/revalidate-test.ts
@@ -1,5 +1,11 @@
+import { createMemoryRouter } from "../../lib/components";
import { IDLE_NAVIGATION } from "../../lib/router";
-import { cleanup, setup, TASK_ROUTES } from "./utils/data-router-setup";
+import {
+ cleanup,
+ setup,
+ createDeferred,
+ TASK_ROUTES,
+} from "./utils/data-router-setup";
import { createFormData } from "./utils/utils";
describe("router.revalidate", () => {
@@ -911,4 +917,217 @@ describe("router.revalidate", () => {
data: "ROOT_DATA**",
});
});
+
+ it("exposes a revalidation promise across navigations", async () => {
+ let revalidationDfd = createDeferred();
+ let navigationDfd = createDeferred();
+ let isFirstCall = true;
+ let router = createMemoryRouter(
+ [
+ {
+ id: "root",
+ loader() {
+ if (isFirstCall) {
+ isFirstCall = false;
+ return revalidationDfd.promise;
+ } else {
+ return navigationDfd.promise;
+ }
+ },
+ children: [
+ {
+ index: true,
+ },
+ {
+ path: "/page",
+ },
+ ],
+ },
+ ],
+ {
+ hydrationData: {
+ loaderData: { root: "ROOT" },
+ },
+ }
+ );
+
+ let revalidationValue = null;
+ let navigationValue = null;
+
+ await router.initialize();
+ expect(router.state).toMatchObject({
+ location: { pathname: "/" },
+ navigation: { state: "idle" },
+ revalidation: "idle",
+ loaderData: { root: "ROOT" },
+ });
+ expect(isFirstCall).toBe(true);
+
+ // Start a revalidation, but don't resolve the revalidation loader call
+ router.revalidate().then(() => {
+ revalidationValue = router.state.loaderData.root;
+ });
+ expect(router.state).toMatchObject({
+ location: { pathname: "/" },
+ navigation: { state: "idle" },
+ revalidation: "loading",
+ loaderData: { root: "ROOT" },
+ });
+ expect(isFirstCall).toBe(false);
+
+ // Interrupt with a GET navigation
+ router.navigate("/page").then(() => {
+ navigationValue = router.state.loaderData.root;
+ });
+ expect(router.state).toMatchObject({
+ location: { pathname: "/" },
+ navigation: { state: "loading" },
+ revalidation: "loading",
+ loaderData: { root: "ROOT" },
+ });
+ expect(revalidationValue).toBeNull();
+ expect(navigationValue).toBeNull();
+
+ // Complete the navigation, which should resolve the revalidation
+ await navigationDfd.resolve("ROOT*");
+ expect(router.state).toMatchObject({
+ location: { pathname: "/page" },
+ navigation: { state: "idle" },
+ revalidation: "idle",
+ loaderData: { root: "ROOT*" },
+ });
+ expect(revalidationValue).toBe("ROOT*");
+ expect(navigationValue).toBe("ROOT*");
+
+ // no-op
+ revalidationDfd.reject();
+ expect(router.state).toMatchObject({
+ location: { pathname: "/page" },
+ navigation: { state: "idle" },
+ revalidation: "idle",
+ loaderData: { root: "ROOT*" },
+ });
+ expect(revalidationValue).toBe("ROOT*");
+ expect(navigationValue).toBe("ROOT*");
+ });
+
+ it("exposes a revalidation promise across multiple navigations", async () => {
+ let revalidationDfd = createDeferred();
+ let navigationDfd = createDeferred();
+ let navigationDfd2 = createDeferred();
+ let count = 0;
+ let router = createMemoryRouter(
+ [
+ {
+ id: "root",
+ loader() {
+ count++;
+ if (count === 1) {
+ return revalidationDfd.promise;
+ } else if (count === 2) {
+ return navigationDfd.promise;
+ } else {
+ return navigationDfd2.promise;
+ }
+ },
+ children: [
+ {
+ index: true,
+ },
+ {
+ path: "/page",
+ },
+ {
+ path: "/page2",
+ },
+ ],
+ },
+ ],
+ {
+ hydrationData: {
+ loaderData: { root: "ROOT" },
+ },
+ }
+ );
+
+ let revalidationValue = null;
+ let navigationValue = null;
+ let navigationValue2 = null;
+
+ await router.initialize();
+ expect(router.state).toMatchObject({
+ location: { pathname: "/" },
+ navigation: { state: "idle" },
+ revalidation: "idle",
+ loaderData: { root: "ROOT" },
+ });
+ expect(count).toBe(0);
+
+ // Start a revalidation, but don't resolve the revalidation loader call
+ router.revalidate().then(() => {
+ revalidationValue = router.state.loaderData.root;
+ });
+ expect(router.state).toMatchObject({
+ location: { pathname: "/" },
+ navigation: { state: "idle" },
+ revalidation: "loading",
+ loaderData: { root: "ROOT" },
+ });
+ expect(count).toBe(1);
+
+ // Interrupt with a navigation
+ router.navigate("/page").then(() => {
+ navigationValue = router.state.loaderData.root;
+ });
+ expect(router.state).toMatchObject({
+ location: { pathname: "/" },
+ navigation: { state: "loading" },
+ revalidation: "loading",
+ loaderData: { root: "ROOT" },
+ });
+ expect(count).toBe(2);
+ expect(revalidationValue).toBeNull();
+ expect(navigationValue).toBeNull();
+ expect(navigationValue2).toBeNull();
+
+ // Interrupt with another navigation
+ router.navigate("/page2").then(() => {
+ navigationValue2 = router.state.loaderData.root;
+ });
+ expect(router.state).toMatchObject({
+ location: { pathname: "/" },
+ navigation: { state: "loading" },
+ revalidation: "loading",
+ loaderData: { root: "ROOT" },
+ });
+ expect(count).toBe(3);
+ expect(revalidationValue).toBeNull();
+ expect(navigationValue).toBeNull();
+ expect(navigationValue2).toBeNull();
+
+ // Complete the navigation, which should resolve the revalidation
+ await navigationDfd2.resolve("ROOT**");
+ expect(router.state).toMatchObject({
+ location: { pathname: "/page2" },
+ navigation: { state: "idle" },
+ revalidation: "idle",
+ loaderData: { root: "ROOT**" },
+ });
+ expect(revalidationValue).toBe("ROOT**");
+ expect(navigationValue).toBe("ROOT**");
+ expect(navigationValue2).toBe("ROOT**");
+
+ // no-op
+ navigationDfd.reject();
+ revalidationDfd.reject();
+ expect(router.state).toMatchObject({
+ location: { pathname: "/page2" },
+ navigation: { state: "idle" },
+ revalidation: "idle",
+ loaderData: { root: "ROOT**" },
+ });
+ expect(revalidationValue).toBe("ROOT**");
+ expect(navigationValue).toBe("ROOT**");
+ expect(navigationValue2).toBe("ROOT**");
+ });
});
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 e58836724a..6a6a09358d 100644
--- a/packages/react-router/__tests__/router/utils/data-router-setup.ts
+++ b/packages/react-router/__tests__/router/utils/data-router-setup.ts
@@ -142,7 +142,6 @@ type SetupOpts = {
initialEntries?: InitialEntry[];
initialIndex?: number;
hydrationData?: HydrationState;
- future?: FutureConfig;
dataStrategy?: DataStrategyFunction;
future?: Partial;
};
diff --git a/packages/react-router/__tests__/useNavigate-test.tsx b/packages/react-router/__tests__/useNavigate-test.tsx
index 528174ed05..66823db747 100644
--- a/packages/react-router/__tests__/useNavigate-test.tsx
+++ b/packages/react-router/__tests__/useNavigate-test.tsx
@@ -14,7 +14,7 @@ import {
} from "react-router";
describe("useNavigate", () => {
- it("navigates to the new location", () => {
+ it("navigates to the new location", async () => {
function Home() {
let navigate = useNavigate();
@@ -44,7 +44,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -54,7 +54,7 @@ describe("useNavigate", () => {
`);
});
- it("navigates to the new location when no pathname is provided", () => {
+ it("navigates to the new location when no pathname is provided", async () => {
function Home() {
let location = useLocation();
let navigate = useNavigate();
@@ -94,7 +94,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -111,7 +111,7 @@ describe("useNavigate", () => {
`);
});
- it("navigates to the new location when no pathname is provided (with a basename)", () => {
+ it("navigates to the new location when no pathname is provided (with a basename)", async () => {
function Home() {
let location = useLocation();
let navigate = useNavigate();
@@ -151,7 +151,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -168,7 +168,7 @@ describe("useNavigate", () => {
`);
});
- it("navigates to the new location with empty query string when no query string is provided", () => {
+ it("navigates to the new location with empty query string when no query string is provided", async () => {
function Home() {
let location = useLocation();
let navigate = useNavigate();
@@ -208,7 +208,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -301,7 +301,7 @@ describe("useNavigate", () => {
);
});
- it("allows useNavigate usage in a mixed RouterProvider/ scenario", () => {
+ it("allows useNavigate usage in a mixed RouterProvider/ scenario", async () => {
const router = createMemoryRouter([
{
path: "/*",
@@ -378,7 +378,7 @@ describe("useNavigate", () => {
let button = renderer.root.findByProps({
children: "Navigate from RouterProvider",
});
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
expect(router.state.location.pathname).toBe("/page");
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -402,7 +402,7 @@ describe("useNavigate", () => {
button = renderer.root.findByProps({
children: "Navigate from RouterProvider",
});
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
expect(router.state.location.pathname).toBe("/");
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -426,7 +426,7 @@ describe("useNavigate", () => {
button = renderer.root.findByProps({
children: "Navigate /page from Routes",
});
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
expect(router.state.location.pathname).toBe("/page");
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -450,7 +450,7 @@ describe("useNavigate", () => {
button = renderer.root.findByProps({
children: "Navigate /home from Routes",
});
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
expect(router.state.location.pathname).toBe("/");
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -529,7 +529,9 @@ describe("useNavigate", () => {
function Home() {
let navigate = useNavigate();
- React.useEffect(() => navigate("/about"), [navigate]);
+ React.useEffect(() => {
+ navigate("/about");
+ }, [navigate]);
return Home
;
}
@@ -565,7 +567,9 @@ describe("useNavigate", () => {
}
function Child({ onChildRendered }) {
- React.useEffect(() => onChildRendered());
+ React.useEffect(() => {
+ onChildRendered();
+ });
return null;
}
@@ -616,7 +620,9 @@ describe("useNavigate", () => {
index: true,
Component() {
let navigate = useNavigate();
- React.useEffect(() => navigate("/about"), [navigate]);
+ React.useEffect(() => {
+ navigate("/about");
+ }, [navigate]);
return Home
;
},
},
@@ -663,7 +669,9 @@ describe("useNavigate", () => {
});
function Child({ onChildRendered }) {
- React.useEffect(() => onChildRendered());
+ React.useEffect(() => {
+ onChildRendered();
+ });
return null;
}
@@ -678,7 +686,7 @@ describe("useNavigate", () => {
});
describe("with state", () => {
- it("adds the state to location.state", () => {
+ it("adds the state to location.state", async () => {
function Home() {
let navigate = useNavigate();
@@ -712,7 +720,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -726,7 +734,7 @@ describe("useNavigate", () => {
describe("when relative navigation is handled via React Context", () => {
describe("with an absolute href", () => {
- it("navigates to the correct URL", () => {
+ it("navigates to the correct URL", async () => {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
@@ -744,7 +752,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -756,7 +764,7 @@ describe("useNavigate", () => {
});
describe("with a relative href (relative=route)", () => {
- it("navigates to the correct URL", () => {
+ it("navigates to the correct URL", async () => {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
@@ -774,7 +782,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -784,7 +792,7 @@ describe("useNavigate", () => {
`);
});
- it("handles upward navigation from an index routes", () => {
+ it("handles upward navigation from an index routes", async () => {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
@@ -801,7 +809,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -811,7 +819,7 @@ describe("useNavigate", () => {
`);
});
- it("handles upward navigation from inside a pathless layout route", () => {
+ it("handles upward navigation from inside a pathless layout route", async () => {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
@@ -831,7 +839,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -841,7 +849,7 @@ describe("useNavigate", () => {
`);
});
- it("handles upward navigation from inside multiple pathless layout routes + index route", () => {
+ it("handles upward navigation from inside multiple pathless layout routes + index route", async () => {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
@@ -867,7 +875,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -877,7 +885,7 @@ describe("useNavigate", () => {
`);
});
- it("handles upward navigation from inside multiple pathless layout routes + path route", () => {
+ it("handles upward navigation from inside multiple pathless layout routes + path route", async () => {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
@@ -903,7 +911,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -913,7 +921,7 @@ describe("useNavigate", () => {
`);
});
- it("handles parent navigation from inside multiple pathless layout routes", () => {
+ it("handles parent navigation from inside multiple pathless layout routes", async () => {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
@@ -952,7 +960,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -962,7 +970,7 @@ describe("useNavigate", () => {
`);
});
- it("handles relative navigation from nested index route", () => {
+ it("handles relative navigation from nested index route", async () => {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
@@ -982,7 +990,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -994,7 +1002,7 @@ describe("useNavigate", () => {
});
describe("with a relative href (relative=path)", () => {
- it("navigates to the correct URL", () => {
+ it("navigates to the correct URL", async () => {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
@@ -1012,7 +1020,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -1022,7 +1030,7 @@ describe("useNavigate", () => {
`);
});
- it("handles upward navigation from an index routes", () => {
+ it("handles upward navigation from an index routes", async () => {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
@@ -1042,7 +1050,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -1052,7 +1060,7 @@ describe("useNavigate", () => {
`);
});
- it("handles upward navigation from inside a pathless layout route", () => {
+ it("handles upward navigation from inside a pathless layout route", async () => {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
@@ -1072,7 +1080,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -1082,7 +1090,7 @@ describe("useNavigate", () => {
`);
});
- it("handles upward navigation from inside multiple pathless layout routes + index route", () => {
+ it("handles upward navigation from inside multiple pathless layout routes + index route", async () => {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
@@ -1110,7 +1118,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -1120,7 +1128,7 @@ describe("useNavigate", () => {
`);
});
- it("handles upward navigation from inside multiple pathless layout routes + path route", () => {
+ it("handles upward navigation from inside multiple pathless layout routes + path route", async () => {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
@@ -1148,7 +1156,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -1158,7 +1166,7 @@ describe("useNavigate", () => {
`);
});
- it("handles relative navigation from nested index route", () => {
+ it("handles relative navigation from nested index route", async () => {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
@@ -1181,7 +1189,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -1191,7 +1199,7 @@ describe("useNavigate", () => {
`);
});
- it("preserves search params and hash", () => {
+ it("preserves search params and hash", async () => {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
@@ -1224,7 +1232,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -1367,7 +1375,7 @@ describe("useNavigate", () => {
describe("when relative navigation is handled via @remix-run/router", () => {
describe("with an absolute href", () => {
- it("navigates to the correct URL", () => {
+ it("navigates to the correct URL", async () => {
let router = createMemoryRouter(
createRoutesFromElements(
<>
@@ -1385,7 +1393,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -1397,7 +1405,7 @@ describe("useNavigate", () => {
});
describe("with a relative href (relative=route)", () => {
- it("navigates to the correct URL", () => {
+ it("navigates to the correct URL", async () => {
let router = createMemoryRouter(
createRoutesFromElements(
<>
@@ -1418,7 +1426,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -1428,7 +1436,7 @@ describe("useNavigate", () => {
`);
});
- it("handles upward navigation from an index routes", () => {
+ it("handles upward navigation from an index routes", async () => {
let router = createMemoryRouter(
createRoutesFromElements(
<>
@@ -1448,7 +1456,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -1458,7 +1466,7 @@ describe("useNavigate", () => {
`);
});
- it("handles upward navigation from inside a pathless layout route", () => {
+ it("handles upward navigation from inside a pathless layout route", async () => {
let router = createMemoryRouter(
createRoutesFromElements(
<>
@@ -1481,7 +1489,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -1491,7 +1499,7 @@ describe("useNavigate", () => {
`);
});
- it("handles upward navigation from inside multiple pathless layout routes + index route", () => {
+ it("handles upward navigation from inside multiple pathless layout routes + index route", async () => {
let router = createMemoryRouter(
createRoutesFromElements(
<>
@@ -1520,7 +1528,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -1530,7 +1538,7 @@ describe("useNavigate", () => {
`);
});
- it("handles upward navigation from inside multiple pathless layout routes + path route", () => {
+ it("handles upward navigation from inside multiple pathless layout routes + path route", async () => {
let router = createMemoryRouter(
createRoutesFromElements(
<>
@@ -1559,7 +1567,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -1569,7 +1577,7 @@ describe("useNavigate", () => {
`);
});
- it("handles parent navigation from inside multiple pathless layout routes", () => {
+ it("handles parent navigation from inside multiple pathless layout routes", async () => {
let router = createMemoryRouter(
createRoutesFromElements(
<>
@@ -1611,7 +1619,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -1621,7 +1629,7 @@ describe("useNavigate", () => {
`);
});
- it("handles relative navigation from nested index route", () => {
+ it("handles relative navigation from nested index route", async () => {
let router = createMemoryRouter(
createRoutesFromElements(
<>
@@ -1644,7 +1652,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -1656,7 +1664,7 @@ describe("useNavigate", () => {
});
describe("with a relative href (relative=path)", () => {
- it("navigates to the correct URL", () => {
+ it("navigates to the correct URL", async () => {
let router = createMemoryRouter(
createRoutesFromElements(
<>
@@ -1677,7 +1685,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -1687,7 +1695,7 @@ describe("useNavigate", () => {
`);
});
- it("handles upward navigation from an index routes", () => {
+ it("handles upward navigation from an index routes", async () => {
let router = createMemoryRouter(
createRoutesFromElements(
<>
@@ -1710,7 +1718,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -1720,7 +1728,7 @@ describe("useNavigate", () => {
`);
});
- it("handles upward navigation from inside a pathless layout route", () => {
+ it("handles upward navigation from inside a pathless layout route", async () => {
let router = createMemoryRouter(
createRoutesFromElements(
<>
@@ -1743,7 +1751,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -1753,7 +1761,7 @@ describe("useNavigate", () => {
`);
});
- it("handles upward navigation from inside multiple pathless layout routes + index route", () => {
+ it("handles upward navigation from inside multiple pathless layout routes + index route", async () => {
let router = createMemoryRouter(
createRoutesFromElements(
<>
@@ -1782,7 +1790,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -1792,7 +1800,7 @@ describe("useNavigate", () => {
`);
});
- it("handles upward navigation from inside multiple pathless layout routes + path route", () => {
+ it("handles upward navigation from inside multiple pathless layout routes + path route", async () => {
let router = createMemoryRouter(
createRoutesFromElements(
<>
@@ -1821,7 +1829,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -1831,7 +1839,7 @@ describe("useNavigate", () => {
`);
});
- it("handles relative navigation from nested index route", () => {
+ it("handles relative navigation from nested index route", async () => {
let router = createMemoryRouter(
createRoutesFromElements(
<>
@@ -1857,7 +1865,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -1867,7 +1875,7 @@ describe("useNavigate", () => {
`);
});
- it("preserves search params and hash", () => {
+ it("preserves search params and hash", async () => {
let router = createMemoryRouter(
createRoutesFromElements(
<>
@@ -1903,7 +1911,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -2054,7 +2062,7 @@ describe("useNavigate", () => {
describe("with a basename", () => {
describe("in a MemoryRouter", () => {
- it("in a root route", () => {
+ it("in a root route", async () => {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
@@ -2081,7 +2089,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -2091,7 +2099,7 @@ describe("useNavigate", () => {
`);
});
- it("in a descendant route", () => {
+ it("in a descendant route", async () => {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
@@ -2125,7 +2133,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -2137,7 +2145,7 @@ describe("useNavigate", () => {
});
describe("in a RouterProvider", () => {
- it("in a root route", () => {
+ it("in a root route", async () => {
let router = createMemoryRouter(
[
{
@@ -2168,7 +2176,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
@@ -2178,7 +2186,7 @@ describe("useNavigate", () => {
`);
});
- it("in a descendant route", () => {
+ it("in a descendant route", async () => {
let router = createMemoryRouter(
[
{
@@ -2215,7 +2223,7 @@ describe("useNavigate", () => {
// @ts-expect-error
let button = renderer.root.findByType("button");
- TestRenderer.act(() => button.props.onClick());
+ await TestRenderer.act(() => button.props.onClick());
// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx
index 7ef1f634f7..1230997201 100644
--- a/packages/react-router/lib/components.tsx
+++ b/packages/react-router/lib/components.tsx
@@ -417,10 +417,9 @@ export function Navigate({
);
let jsonPath = JSON.stringify(path);
- React.useEffect(
- () => navigate(JSON.parse(jsonPath), { replace, state, relative }),
- [navigate, jsonPath, relative, replace, state]
- );
+ React.useEffect(() => {
+ navigate(JSON.parse(jsonPath), { replace, state, relative });
+ }, [navigate, jsonPath, relative, replace, state]);
return null;
}
diff --git a/packages/react-router/lib/dom/lib.tsx b/packages/react-router/lib/dom/lib.tsx
index cf4ebbf7ad..af7c68c6d5 100644
--- a/packages/react-router/lib/dom/lib.tsx
+++ b/packages/react-router/lib/dom/lib.tsx
@@ -1709,7 +1709,7 @@ export interface SubmitFunction {
* submitting arbitrary data without a backing `