Skip to content

Commit 5e195ec

Browse files
authored
Fix useNAvigate when called from <Routes> inside a <RouterProvider> (#10432)
1 parent 290d9e7 commit 5e195ec

File tree

4 files changed

+188
-4
lines changed

4 files changed

+188
-4
lines changed

.changeset/navigate-from-routes.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router": patch
3+
---
4+
5+
Fix bug when calling `useNavigate` from `<Routes>` inside a `<RouterProvider>`

packages/react-router/__tests__/useNavigate-test.tsx

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
Route,
88
useNavigate,
99
useLocation,
10+
useRoutes,
1011
createMemoryRouter,
1112
createRoutesFromElements,
1213
Outlet,
@@ -301,6 +302,178 @@ describe("useNavigate", () => {
301302
);
302303
});
303304

305+
it("allows useNavigate usage in a mixed RouterProvider/<Routes> scenario", () => {
306+
const router = createMemoryRouter([
307+
{
308+
path: "/*",
309+
Component() {
310+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
311+
let navigate = useNavigate();
312+
let location = useLocation();
313+
return (
314+
<>
315+
<button
316+
onClick={() =>
317+
navigate(location.pathname === "/" ? "/page" : "/")
318+
}
319+
>
320+
Navigate from RouterProvider
321+
</button>
322+
<Routes>
323+
<Route path="/" element={<Home />} />
324+
<Route path="/page" element={<Page />} />
325+
</Routes>
326+
</>
327+
);
328+
},
329+
},
330+
]);
331+
332+
function Home() {
333+
let navigate = useNavigate();
334+
return (
335+
<>
336+
<h1>Home</h1>
337+
<button onClick={() => navigate("/page")}>
338+
Navigate /page from Routes
339+
</button>
340+
</>
341+
);
342+
}
343+
344+
function Page() {
345+
let navigate = useNavigate();
346+
return (
347+
<>
348+
<h1>Page</h1>
349+
<button onClick={() => navigate("/")}>
350+
Navigate /home from Routes
351+
</button>
352+
</>
353+
);
354+
}
355+
356+
let renderer: TestRenderer.ReactTestRenderer;
357+
TestRenderer.act(() => {
358+
renderer = TestRenderer.create(<RouterProvider router={router} />);
359+
});
360+
361+
expect(router.state.location.pathname).toBe("/");
362+
expect(renderer.toJSON()).toMatchInlineSnapshot(`
363+
[
364+
<button
365+
onClick={[Function]}
366+
>
367+
Navigate from RouterProvider
368+
</button>,
369+
<h1>
370+
Home
371+
</h1>,
372+
<button
373+
onClick={[Function]}
374+
>
375+
Navigate /page from Routes
376+
</button>,
377+
]
378+
`);
379+
380+
let button = renderer.root.findByProps({
381+
children: "Navigate from RouterProvider",
382+
});
383+
TestRenderer.act(() => button.props.onClick());
384+
385+
expect(router.state.location.pathname).toBe("/page");
386+
expect(renderer.toJSON()).toMatchInlineSnapshot(`
387+
[
388+
<button
389+
onClick={[Function]}
390+
>
391+
Navigate from RouterProvider
392+
</button>,
393+
<h1>
394+
Page
395+
</h1>,
396+
<button
397+
onClick={[Function]}
398+
>
399+
Navigate /home from Routes
400+
</button>,
401+
]
402+
`);
403+
404+
button = renderer.root.findByProps({
405+
children: "Navigate from RouterProvider",
406+
});
407+
TestRenderer.act(() => button.props.onClick());
408+
409+
expect(router.state.location.pathname).toBe("/");
410+
expect(renderer.toJSON()).toMatchInlineSnapshot(`
411+
[
412+
<button
413+
onClick={[Function]}
414+
>
415+
Navigate from RouterProvider
416+
</button>,
417+
<h1>
418+
Home
419+
</h1>,
420+
<button
421+
onClick={[Function]}
422+
>
423+
Navigate /page from Routes
424+
</button>,
425+
]
426+
`);
427+
428+
button = renderer.root.findByProps({
429+
children: "Navigate /page from Routes",
430+
});
431+
TestRenderer.act(() => button.props.onClick());
432+
433+
expect(router.state.location.pathname).toBe("/page");
434+
expect(renderer.toJSON()).toMatchInlineSnapshot(`
435+
[
436+
<button
437+
onClick={[Function]}
438+
>
439+
Navigate from RouterProvider
440+
</button>,
441+
<h1>
442+
Page
443+
</h1>,
444+
<button
445+
onClick={[Function]}
446+
>
447+
Navigate /home from Routes
448+
</button>,
449+
]
450+
`);
451+
452+
button = renderer.root.findByProps({
453+
children: "Navigate /home from Routes",
454+
});
455+
TestRenderer.act(() => button.props.onClick());
456+
457+
expect(router.state.location.pathname).toBe("/");
458+
expect(renderer.toJSON()).toMatchInlineSnapshot(`
459+
[
460+
<button
461+
onClick={[Function]}
462+
>
463+
Navigate from RouterProvider
464+
</button>,
465+
<h1>
466+
Home
467+
</h1>,
468+
<button
469+
onClick={[Function]}
470+
>
471+
Navigate /page from Routes
472+
</button>,
473+
]
474+
`);
475+
});
476+
304477
describe("navigating in effects versus render", () => {
305478
let warnSpy: jest.SpyInstance;
306479

packages/react-router/lib/context.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,11 +144,13 @@ if (__DEV__) {
144144
export interface RouteContextObject {
145145
outlet: React.ReactElement | null;
146146
matches: RouteMatch[];
147+
isDataRoute: boolean;
147148
}
148149

149150
export const RouteContext = React.createContext<RouteContextObject>({
150151
outlet: null,
151152
matches: [],
153+
isDataRoute: false,
152154
});
153155

154156
if (__DEV__) {

packages/react-router/lib/hooks.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,10 +174,10 @@ function useIsomorphicLayoutEffect(
174174
* @see https://reactrouter.com/hooks/use-navigate
175175
*/
176176
export function useNavigate(): NavigateFunction {
177-
let isDataRouter = React.useContext(DataRouterContext) != null;
177+
let { isDataRoute } = React.useContext(RouteContext);
178178
// Conditional usage is OK here because the usage of a data router is static
179179
// eslint-disable-next-line react-hooks/rules-of-hooks
180-
return isDataRouter ? useNavigateStable() : useNavigateUnstable();
180+
return isDataRoute ? useNavigateStable() : useNavigateUnstable();
181181
}
182182

183183
function useNavigateUnstable(): NavigateFunction {
@@ -705,7 +705,11 @@ export function _renderMatches(
705705
return (
706706
<RenderedRoute
707707
match={match}
708-
routeContext={{ outlet, matches }}
708+
routeContext={{
709+
outlet,
710+
matches,
711+
isDataRoute: dataRouterState != null,
712+
}}
709713
children={children}
710714
/>
711715
);
@@ -721,7 +725,7 @@ export function _renderMatches(
721725
component={errorElement}
722726
error={error}
723727
children={getChildren()}
724-
routeContext={{ outlet: null, matches }}
728+
routeContext={{ outlet: null, matches, isDataRoute: true }}
725729
/>
726730
) : (
727731
getChildren()

0 commit comments

Comments
 (0)