diff --git a/.changeset/navigate-from-routes.md b/.changeset/navigate-from-routes.md new file mode 100644 index 0000000000..335f14b392 --- /dev/null +++ b/.changeset/navigate-from-routes.md @@ -0,0 +1,5 @@ +--- +"react-router": patch +--- + +Fix bug when calling `useNavigate` from `` inside a `` diff --git a/packages/react-router/__tests__/useNavigate-test.tsx b/packages/react-router/__tests__/useNavigate-test.tsx index 6c601a82c5..3e3dead5b4 100644 --- a/packages/react-router/__tests__/useNavigate-test.tsx +++ b/packages/react-router/__tests__/useNavigate-test.tsx @@ -7,6 +7,7 @@ import { Route, useNavigate, useLocation, + useRoutes, createMemoryRouter, createRoutesFromElements, Outlet, @@ -301,6 +302,178 @@ describe("useNavigate", () => { ); }); + it("allows useNavigate usage in a mixed RouterProvider/ scenario", () => { + const router = createMemoryRouter([ + { + path: "/*", + Component() { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + let navigate = useNavigate(); + let location = useLocation(); + return ( + <> + + + } /> + } /> + + + ); + }, + }, + ]); + + function Home() { + let navigate = useNavigate(); + return ( + <> +

Home

+ + + ); + } + + function Page() { + let navigate = useNavigate(); + return ( + <> +

Page

+ + + ); + } + + let renderer: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer = TestRenderer.create(); + }); + + expect(router.state.location.pathname).toBe("/"); + expect(renderer.toJSON()).toMatchInlineSnapshot(` + [ + , +

+ Home +

, + , + ] + `); + + let button = renderer.root.findByProps({ + children: "Navigate from RouterProvider", + }); + TestRenderer.act(() => button.props.onClick()); + + expect(router.state.location.pathname).toBe("/page"); + expect(renderer.toJSON()).toMatchInlineSnapshot(` + [ + , +

+ Page +

, + , + ] + `); + + button = renderer.root.findByProps({ + children: "Navigate from RouterProvider", + }); + TestRenderer.act(() => button.props.onClick()); + + expect(router.state.location.pathname).toBe("/"); + expect(renderer.toJSON()).toMatchInlineSnapshot(` + [ + , +

+ Home +

, + , + ] + `); + + button = renderer.root.findByProps({ + children: "Navigate /page from Routes", + }); + TestRenderer.act(() => button.props.onClick()); + + expect(router.state.location.pathname).toBe("/page"); + expect(renderer.toJSON()).toMatchInlineSnapshot(` + [ + , +

+ Page +

, + , + ] + `); + + button = renderer.root.findByProps({ + children: "Navigate /home from Routes", + }); + TestRenderer.act(() => button.props.onClick()); + + expect(router.state.location.pathname).toBe("/"); + expect(renderer.toJSON()).toMatchInlineSnapshot(` + [ + , +

+ Home +

, + , + ] + `); + }); + describe("navigating in effects versus render", () => { let warnSpy: jest.SpyInstance; diff --git a/packages/react-router/lib/context.ts b/packages/react-router/lib/context.ts index d8660ebd1e..8e5951154c 100644 --- a/packages/react-router/lib/context.ts +++ b/packages/react-router/lib/context.ts @@ -144,11 +144,13 @@ if (__DEV__) { export interface RouteContextObject { outlet: React.ReactElement | null; matches: RouteMatch[]; + isDataRoute: boolean; } export const RouteContext = React.createContext({ outlet: null, matches: [], + isDataRoute: false, }); if (__DEV__) { diff --git a/packages/react-router/lib/hooks.tsx b/packages/react-router/lib/hooks.tsx index be846ef51a..5121e0b2e2 100644 --- a/packages/react-router/lib/hooks.tsx +++ b/packages/react-router/lib/hooks.tsx @@ -174,10 +174,10 @@ function useIsomorphicLayoutEffect( * @see https://reactrouter.com/hooks/use-navigate */ export function useNavigate(): NavigateFunction { - let isDataRouter = React.useContext(DataRouterContext) != null; + let { isDataRoute } = React.useContext(RouteContext); // Conditional usage is OK here because the usage of a data router is static // eslint-disable-next-line react-hooks/rules-of-hooks - return isDataRouter ? useNavigateStable() : useNavigateUnstable(); + return isDataRoute ? useNavigateStable() : useNavigateUnstable(); } function useNavigateUnstable(): NavigateFunction { @@ -697,7 +697,11 @@ export function _renderMatches( return ( ); @@ -713,7 +717,7 @@ export function _renderMatches( component={errorElement} error={error} children={getChildren()} - routeContext={{ outlet: null, matches }} + routeContext={{ outlet: null, matches, isDataRoute: true }} /> ) : ( getChildren()