Skip to content

Commit aea5f22

Browse files
committed
fix(react-router): Don't add child routes to Set on root-level
1 parent 910b40b commit aea5f22

File tree

2 files changed

+251
-7
lines changed

2 files changed

+251
-7
lines changed

packages/react/src/reactrouter-compat-utils/instrumentation.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ export interface ReactRouterOptions {
106106
type V6CompatibleVersion = '6' | '7';
107107

108108
// Keeping as a global variable for cross-usage in multiple functions
109-
const allRoutes = new Set<RouteObject>();
109+
// only exported for testing purposes
110+
export const allRoutes = new Set<RouteObject>();
110111

111112
/**
112113
* Processes resolved routes by adding them to allRoutes and checking for nested async handlers.
@@ -679,13 +680,12 @@ export function handleNavigation(opts: {
679680
}
680681
}
681682

682-
function addRoutesToAllRoutes(routes: RouteObject[]): void {
683+
/* Only exported for testing purposes */
684+
export function addRoutesToAllRoutes(routes: RouteObject[]): void {
683685
routes.forEach(route => {
684-
const extractedChildRoutes = getChildRoutesRecursively(route);
685-
686-
extractedChildRoutes.forEach(r => {
687-
allRoutes.add(r);
688-
});
686+
if (!allRoutes.has(route)) {
687+
allRoutes.add(route);
688+
}
689689
});
690690
}
691691

packages/react/test/reactrouter-compat-utils/instrumentation.test.tsx

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
createReactRouterV6CompatibleTracingIntegration,
1111
updateNavigationSpan,
1212
} from '../../src/reactrouter-compat-utils';
13+
import { addRoutesToAllRoutes, allRoutes } from '../../src/reactrouter-compat-utils/instrumentation';
1314
import type { Location, RouteObject } from '../../src/types';
1415

1516
const mockUpdateName = vi.fn();
@@ -141,3 +142,246 @@ describe('reactrouter-compat-utils/instrumentation', () => {
141142
});
142143
});
143144
});
145+
146+
describe('addRoutesToAllRoutes', () => {
147+
beforeEach(() => {
148+
vi.clearAllMocks();
149+
vi.resetModules();
150+
allRoutes.clear();
151+
});
152+
153+
it('should add simple routes without nesting', () => {
154+
const routes = [
155+
{ path: '/', element: <div /> },
156+
{ path: '/user/:id', element: <div /> },
157+
{ path: '/group/:group/:user?', element: <div /> },
158+
];
159+
160+
addRoutesToAllRoutes(routes);
161+
const allRoutesArr = Array.from(allRoutes);
162+
163+
expect(allRoutesArr).toHaveLength(3);
164+
expect(allRoutesArr).toEqual(
165+
expect.arrayContaining([
166+
expect.objectContaining({ path: '/' }),
167+
expect.objectContaining({ path: '/user/:id' }),
168+
expect.objectContaining({ path: '/group/:group/:user?' }),
169+
]),
170+
);
171+
172+
// Verify exact structure matches manual testing results
173+
allRoutesArr.forEach(route => {
174+
expect(route).toHaveProperty('element');
175+
expect(route.element).toHaveProperty('props');
176+
});
177+
});
178+
179+
it('should preserve nested children on parent routes without creating duplicate top-level entries', () => {
180+
const routes = [
181+
{ path: '/', element: <div /> },
182+
{ path: '/user/:id', element: <div /> },
183+
{ path: '/group/:group/:user?', element: <div /> },
184+
{
185+
path: '/v2/post/:post',
186+
element: <div />,
187+
children: [
188+
{ index: true, element: <div /> },
189+
{ path: 'featured', element: <div /> },
190+
{ path: '/v2/post/:post/related', element: <div /> },
191+
],
192+
},
193+
];
194+
195+
addRoutesToAllRoutes(routes);
196+
const allRoutesArr = Array.from(allRoutes);
197+
198+
// Should have all routes flattened (parent + all descendants)
199+
expect(allRoutesArr.length).toBeGreaterThanOrEqual(4);
200+
201+
// Find the parent route
202+
const parentRoute = allRoutesArr.find((r: any) => r.path === '/v2/post/:post');
203+
expect(parentRoute).toBeTruthy();
204+
expect(parentRoute!.children).toBeDefined();
205+
expect(Array.isArray(parentRoute!.children)).toBe(true);
206+
expect(parentRoute!.children).toHaveLength(3);
207+
208+
// Verify children structure is preserved
209+
expect(parentRoute!.children).toEqual(
210+
expect.arrayContaining([
211+
expect.objectContaining({ index: true }),
212+
expect.objectContaining({ path: 'featured' }),
213+
expect.objectContaining({ path: '/v2/post/:post/related' }),
214+
]),
215+
);
216+
217+
// Verify that children are NOT in allRoutes as individual entries
218+
expect(allRoutesArr).not.toEqual(
219+
expect.arrayContaining([
220+
expect.objectContaining({ index: true }),
221+
expect.objectContaining({ path: 'featured' }),
222+
expect.objectContaining({ path: '/v2/post/:post/related' }),
223+
]),
224+
);
225+
});
226+
227+
it('should handle complex nested routes with multiple levels', () => {
228+
const routes = [
229+
{ path: '/', element: <div /> },
230+
{ path: '/user/:id', element: <div /> },
231+
{ path: '/group/:group/:user?', element: <div /> },
232+
{
233+
path: '/v1/post/:post',
234+
element: <div />,
235+
children: [
236+
{ path: 'featured', element: <div /> },
237+
{ path: '/v1/post/:post/related', element: <div /> },
238+
{
239+
element: <div>More Nested Children</div>,
240+
children: [{ path: 'edit', element: <div>Edit Post</div> }],
241+
},
242+
],
243+
},
244+
{
245+
path: '/v2/post/:post',
246+
element: <div />,
247+
children: [
248+
{ index: true, element: <div /> },
249+
{ path: 'featured', element: <div /> },
250+
{ path: '/v2/post/:post/related', element: <div /> },
251+
],
252+
},
253+
];
254+
255+
addRoutesToAllRoutes(routes);
256+
const allRoutesArr = Array.from(allRoutes);
257+
258+
expect(allRoutesArr).toEqual([
259+
{
260+
path: '/',
261+
element: expect.objectContaining({ type: 'div', props: {} }),
262+
},
263+
{
264+
path: '/user/:id',
265+
element: expect.objectContaining({ type: 'div', props: {} }),
266+
},
267+
{
268+
path: '/group/:group/:user?',
269+
element: expect.objectContaining({ type: 'div', props: {} }),
270+
},
271+
{
272+
path: '/v1/post/:post',
273+
element: expect.objectContaining({ type: 'div', props: {} }),
274+
children: [
275+
{ element: <div />, path: 'featured' },
276+
{ element: <div />, path: '/v1/post/:post/related' },
277+
{ children: [{ element: <div>Edit Post</div>, path: 'edit' }], element: <div>More Nested Children</div> },
278+
],
279+
},
280+
{
281+
path: '/v2/post/:post',
282+
element: expect.objectContaining({ type: 'div', props: {} }),
283+
children: [
284+
{ element: <div />, index: true },
285+
{ element: <div />, path: 'featured' },
286+
{ element: <div />, path: '/v2/post/:post/related' },
287+
],
288+
},
289+
]);
290+
});
291+
292+
it('should handle routes with nested index routes', () => {
293+
const routes = [
294+
{
295+
path: '/dashboard',
296+
element: <div />,
297+
children: [
298+
{ index: true, element: <div>Dashboard Index</div> },
299+
{ path: 'settings', element: <div>Settings</div> },
300+
],
301+
},
302+
];
303+
304+
addRoutesToAllRoutes(routes);
305+
const allRoutesArr = Array.from(allRoutes);
306+
307+
expect(allRoutesArr).toEqual([
308+
{
309+
path: '/dashboard',
310+
element: expect.objectContaining({ type: 'div' }),
311+
children: [
312+
{ element: <div>Dashboard Index</div>, index: true },
313+
{ element: <div>Settings</div>, path: 'settings' },
314+
],
315+
},
316+
]);
317+
});
318+
319+
it('should handle deeply nested routes with layout wrappers', () => {
320+
const routes = [
321+
{
322+
path: '/',
323+
element: <div>Root</div>,
324+
children: [
325+
{ path: 'contact', element: <div>Contact</div> },
326+
{ path: 'dashboard', element: <div>Dashboard</div> },
327+
{
328+
element: <div>AuthLayout</div>,
329+
children: [
330+
{ path: 'login', element: <div>Login</div> },
331+
{ path: 'logout', element: <div>Logout</div> },
332+
],
333+
},
334+
],
335+
},
336+
];
337+
338+
addRoutesToAllRoutes(routes);
339+
const allRoutesArr = Array.from(allRoutes);
340+
341+
// Verify all routes are flattened into allRoutes
342+
expect(allRoutesArr).toEqual([
343+
{
344+
path: '/',
345+
element: expect.objectContaining({ type: 'div', props: { children: 'Root' } }),
346+
children: [
347+
{
348+
path: 'contact',
349+
element: expect.objectContaining({ type: 'div', props: { children: 'Contact' } }),
350+
},
351+
{
352+
path: 'dashboard',
353+
element: expect.objectContaining({ type: 'div', props: { children: 'Dashboard' } }),
354+
},
355+
{
356+
element: expect.objectContaining({ type: 'div', props: { children: 'AuthLayout' } }),
357+
children: [
358+
{
359+
path: 'login',
360+
element: expect.objectContaining({ type: 'div', props: { children: 'Login' } }),
361+
},
362+
{
363+
path: 'logout',
364+
element: expect.objectContaining({ type: 'div', props: { children: 'Logout' } }),
365+
},
366+
],
367+
},
368+
],
369+
},
370+
]);
371+
});
372+
373+
it('should not duplicate routes when called multiple times', () => {
374+
const routes = [
375+
{ path: '/', element: <div /> },
376+
{ path: '/about', element: <div /> },
377+
];
378+
379+
addRoutesToAllRoutes(routes);
380+
const firstCount = allRoutes.size;
381+
382+
addRoutesToAllRoutes(routes);
383+
const secondCount = allRoutes.size;
384+
385+
expect(firstCount).toBe(secondCount);
386+
});
387+
});

0 commit comments

Comments
 (0)