Skip to content

Commit 93a9ed2

Browse files
committed
fix(routing): notify about double routing roots
Having two routing roots on the page at once causes problems, as they are both listening to the same routing events If they have the same routes supplied it's probably ok, just double broadcast messages. But if they have different routes then one will broadcast a 404 event, and one will not. Better not to do it at all. So that will now throw an error in dev and log an error message in prod to enable it to be caught
1 parent e05fef3 commit 93a9ed2

File tree

8 files changed

+115
-11
lines changed

8 files changed

+115
-11
lines changed

package-lock.json

Lines changed: 7 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"@testing-library/user-event": "^14.4.3",
3838
"@types/history": "^4.7.7",
3939
"@types/jest": "^28.1.4",
40+
"@types/node": "^20.4.9",
4041
"@types/react": "^17.0.29",
4142
"@types/react-dom": "^18.0.6",
4243
"@types/testing-library__jest-dom": "^5.14.1",
@@ -72,9 +73,9 @@
7273
},
7374
"peerDependencies": {
7475
"@xstate/react": "^3.x",
76+
"react": ">= 16.8.0 < 19.0.0",
7577
"xstate": ">= 4.20 < 5.0.0",
76-
"zod": "^3.x",
77-
"react": ">= 16.8.0 < 19.0.0"
78+
"zod": "^3.x"
7879
},
7980
"scripts": {
8081
"lint": "eslint 'src/**/*'",

src/routing/Link.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export function Link<TRoute extends AnyRoute>({
6161
// and everything that consumes params/query already checks for undefined
6262
const { params, query, meta, ...props } = rest;
6363

64-
let timeout: number | undefined;
64+
let timeout: ReturnType<typeof setTimeout> | undefined;
6565
const href = useHref(to, params, query);
6666
const onMouseDown: React.MouseEventHandler<HTMLAnchorElement> | undefined =
6767
preloadOnInteraction

src/routing/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,8 @@ export {
2323
export { useIsRouteActive } from "./useIsRouteActive";
2424
export { useRouteArgsIfActive } from "./useRouteArgsIfActive";
2525

26-
export { RoutingContext, TestRoutingContext } from "./providers";
26+
export {
27+
RoutingContext,
28+
TestRoutingContext,
29+
useInRoutingContext,
30+
} from "./providers";

src/routing/providers.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ function useRoutingContext() {
2121
return context;
2222
}
2323

24+
/**
25+
* @private
26+
*/
27+
export function useInRoutingContext(): boolean {
28+
const context = useContext(RoutingContext);
29+
30+
return context !== undefined;
31+
}
32+
2433
export function useActiveRouteEvents() {
2534
try {
2635
const context = useRoutingContext();

src/xstateTree.spec.tsx

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { render } from "@testing-library/react";
22
import { assign } from "@xstate/immer";
3+
import { createMemoryHistory } from "history";
34
import React from "react";
45
import { createMachine, interpret } from "xstate";
56

@@ -262,4 +263,81 @@ describe("xstate-tree", () => {
262263
expect(view2).toBe(getMultiSlotViewForChildren(interpreter2, "ignored"));
263264
});
264265
});
266+
267+
describe("rendering a root inside of a root", () => {
268+
it("throws an error during rendering if both are routing roots", async () => {
269+
const machine = createMachine({
270+
id: "test",
271+
initial: "idle",
272+
states: {
273+
idle: {},
274+
},
275+
});
276+
277+
const RootMachine = createXStateTreeMachine(machine, {
278+
View() {
279+
return <p>I am root</p>;
280+
},
281+
});
282+
const Root = buildRootComponent(RootMachine, {
283+
basePath: "/",
284+
history: createMemoryHistory(),
285+
routes: [],
286+
});
287+
288+
const Root2Machine = createXStateTreeMachine(machine, {
289+
View() {
290+
return <Root />;
291+
},
292+
});
293+
const Root2 = buildRootComponent(Root2Machine, {
294+
basePath: "/",
295+
history: createMemoryHistory(),
296+
routes: [],
297+
});
298+
299+
try {
300+
const { rerender } = render(<Root2 />);
301+
rerender(<Root2 />);
302+
} catch (e: any) {
303+
expect(e.message).toMatchInlineSnapshot(
304+
`"Routing root rendered inside routing context, this implies a bug"`
305+
);
306+
return;
307+
}
308+
309+
throw new Error("Should have thrown");
310+
});
311+
312+
it("does not throw an error if either or one are a routing root", async () => {
313+
const machine = createMachine({
314+
id: "test",
315+
initial: "idle",
316+
states: {
317+
idle: {},
318+
},
319+
});
320+
321+
const RootMachine = createXStateTreeMachine(machine, {
322+
View() {
323+
return <p>I am root</p>;
324+
},
325+
});
326+
const Root = buildRootComponent(RootMachine);
327+
328+
const Root2Machine = createXStateTreeMachine(machine, {
329+
View() {
330+
return <Root />;
331+
},
332+
});
333+
const Root2 = buildRootComponent(Root2Machine, {
334+
basePath: "/",
335+
history: createMemoryHistory(),
336+
routes: [],
337+
});
338+
339+
const { rerender } = render(<Root2 />);
340+
rerender(<Root2 />);
341+
});
342+
});
265343
});

src/xstateTree.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
RoutingContext,
2424
RoutingEvent,
2525
SharedMeta,
26+
useInRoutingContext,
2627
} from "./routing";
2728
import { useActiveRouteEvents } from "./routing/providers";
2829
import { GetSlotNames, Slot } from "./slots";
@@ -353,6 +354,16 @@ export function buildRootComponent(
353354
const setActiveRouteEvents = (events: RoutingEvent<any>[]) => {
354355
activeRouteEventsRef.current = events;
355356
};
357+
const insideRoutingContext = useInRoutingContext();
358+
if (insideRoutingContext && typeof routing !== "undefined") {
359+
const m =
360+
"Routing root rendered inside routing context, this implies a bug";
361+
if (process.env.NODE_ENV !== "production") {
362+
throw new Error(m);
363+
}
364+
365+
console.error(m);
366+
}
356367

357368
useEffect(() => {
358369
function handler(event: GlobalEvents) {

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"moduleResolution": "node",
2020
"esModuleInterop": true,
2121
"declarationMap": true,
22-
"types": ["jest"],
22+
"types": ["jest", "node"],
2323
"paths": {
2424
"@koordinates/xstate-tree": ["./src/index.ts"]
2525
},

0 commit comments

Comments
 (0)