Skip to content

Commit

Permalink
Fix useNAvigate when called from <Routes> inside a <RouterProvider> (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
brophdawg11 authored May 2, 2023
1 parent 290d9e7 commit 5e195ec
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .changeset/navigate-from-routes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-router": patch
---

Fix bug when calling `useNavigate` from `<Routes>` inside a `<RouterProvider>`
173 changes: 173 additions & 0 deletions packages/react-router/__tests__/useNavigate-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Route,
useNavigate,
useLocation,
useRoutes,
createMemoryRouter,
createRoutesFromElements,
Outlet,
Expand Down Expand Up @@ -301,6 +302,178 @@ describe("useNavigate", () => {
);
});

it("allows useNavigate usage in a mixed RouterProvider/<Routes> scenario", () => {
const router = createMemoryRouter([
{
path: "/*",
Component() {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let navigate = useNavigate();
let location = useLocation();
return (
<>
<button
onClick={() =>
navigate(location.pathname === "/" ? "/page" : "/")
}
>
Navigate from RouterProvider
</button>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/page" element={<Page />} />
</Routes>
</>
);
},
},
]);

function Home() {
let navigate = useNavigate();
return (
<>
<h1>Home</h1>
<button onClick={() => navigate("/page")}>
Navigate /page from Routes
</button>
</>
);
}

function Page() {
let navigate = useNavigate();
return (
<>
<h1>Page</h1>
<button onClick={() => navigate("/")}>
Navigate /home from Routes
</button>
</>
);
}

let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(<RouterProvider router={router} />);
});

expect(router.state.location.pathname).toBe("/");
expect(renderer.toJSON()).toMatchInlineSnapshot(`
[
<button
onClick={[Function]}
>
Navigate from RouterProvider
</button>,
<h1>
Home
</h1>,
<button
onClick={[Function]}
>
Navigate /page from Routes
</button>,
]
`);

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(`
[
<button
onClick={[Function]}
>
Navigate from RouterProvider
</button>,
<h1>
Page
</h1>,
<button
onClick={[Function]}
>
Navigate /home from Routes
</button>,
]
`);

button = renderer.root.findByProps({
children: "Navigate from RouterProvider",
});
TestRenderer.act(() => button.props.onClick());

expect(router.state.location.pathname).toBe("/");
expect(renderer.toJSON()).toMatchInlineSnapshot(`
[
<button
onClick={[Function]}
>
Navigate from RouterProvider
</button>,
<h1>
Home
</h1>,
<button
onClick={[Function]}
>
Navigate /page from Routes
</button>,
]
`);

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(`
[
<button
onClick={[Function]}
>
Navigate from RouterProvider
</button>,
<h1>
Page
</h1>,
<button
onClick={[Function]}
>
Navigate /home from Routes
</button>,
]
`);

button = renderer.root.findByProps({
children: "Navigate /home from Routes",
});
TestRenderer.act(() => button.props.onClick());

expect(router.state.location.pathname).toBe("/");
expect(renderer.toJSON()).toMatchInlineSnapshot(`
[
<button
onClick={[Function]}
>
Navigate from RouterProvider
</button>,
<h1>
Home
</h1>,
<button
onClick={[Function]}
>
Navigate /page from Routes
</button>,
]
`);
});

describe("navigating in effects versus render", () => {
let warnSpy: jest.SpyInstance;

Expand Down
2 changes: 2 additions & 0 deletions packages/react-router/lib/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,13 @@ if (__DEV__) {
export interface RouteContextObject {
outlet: React.ReactElement | null;
matches: RouteMatch[];
isDataRoute: boolean;
}

export const RouteContext = React.createContext<RouteContextObject>({
outlet: null,
matches: [],
isDataRoute: false,
});

if (__DEV__) {
Expand Down
12 changes: 8 additions & 4 deletions packages/react-router/lib/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -705,7 +705,11 @@ export function _renderMatches(
return (
<RenderedRoute
match={match}
routeContext={{ outlet, matches }}
routeContext={{
outlet,
matches,
isDataRoute: dataRouterState != null,
}}
children={children}
/>
);
Expand All @@ -721,7 +725,7 @@ export function _renderMatches(
component={errorElement}
error={error}
children={getChildren()}
routeContext={{ outlet: null, matches }}
routeContext={{ outlet: null, matches, isDataRoute: true }}
/>
) : (
getChildren()
Expand Down

0 comments on commit 5e195ec

Please sign in to comment.