diff --git a/src/index.ts b/src/index.ts index 7bd5679..bae11ee 100644 --- a/src/index.ts +++ b/src/index.ts @@ -32,6 +32,7 @@ export { useRouteArgsIfActive, useActiveRouteEvents, TestRoutingContext, + useOnRoute, } from "./routing"; export { loggingMetaOptions } from "./useService"; export { lazy } from "./lazy"; diff --git a/src/routing/index.ts b/src/routing/index.ts index b04fda2..5b2eef9 100644 --- a/src/routing/index.ts +++ b/src/routing/index.ts @@ -23,6 +23,7 @@ export { } from "./handleLocationChange"; export { useIsRouteActive } from "./useIsRouteActive"; export { useRouteArgsIfActive } from "./useRouteArgsIfActive"; +export { useOnRoute } from "./useOnRoute"; export { RoutingContext, diff --git a/src/routing/useOnRoute.spec.tsx b/src/routing/useOnRoute.spec.tsx new file mode 100644 index 0000000..4ecf19a --- /dev/null +++ b/src/routing/useOnRoute.spec.tsx @@ -0,0 +1,109 @@ +import { renderHook } from "@testing-library/react"; +import { createMemoryHistory } from "history"; +import React from "react"; + +import { buildCreateRoute } from "./createRoute"; +import { RoutingContext } from "./providers"; +import { useOnRoute } from "./useOnRoute"; + +const createRoute = buildCreateRoute(() => createMemoryHistory(), "/"); +const fooRoute = createRoute.simpleRoute()({ + event: "foo", + url: "/", +}); + +describe("useOnRoute", () => { + it("returns false if the supplied route is not part of the activeRouteEvents in the routing context", () => { + const { result } = renderHook(() => useOnRoute(fooRoute), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + expect(result.current).toBe(false); + }); + + it("throws an error if not called within the RoutingContext", () => { + expect(() => renderHook(() => useOnRoute(fooRoute))).toThrow(); + }); + + it("returns false if the supplied route is part of the activeRouteEvents but not marked as an index event", () => { + const { result } = renderHook(() => useOnRoute(fooRoute), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + expect(result.current).toBe(false); + }); + + it("returns true if the supplied route is part of the activeRouteEvents and marked as an index event", () => { + const { result } = renderHook(() => useOnRoute(fooRoute), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + expect(result.current).toBe(true); + }); + + it("returns false if a different route is active and marked as an index event", () => { + const { result } = renderHook(() => useOnRoute(fooRoute), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + expect(result.current).toBe(false); + }); +}); diff --git a/src/routing/useOnRoute.tsx b/src/routing/useOnRoute.tsx new file mode 100644 index 0000000..d4d4fb4 --- /dev/null +++ b/src/routing/useOnRoute.tsx @@ -0,0 +1,28 @@ +import { AnyRoute, type SharedMeta } from "./createRoute"; +import { useActiveRouteEvents } from "./providers"; + +/** + * @public + * Accepts a single Route and returns true if the route is currently active and marked as an index route. + * False if not. + * + * If used outside of a RoutingContext, an error will be thrown. + * @param route - the route to check + * @returns true if the route is active and an index route, false if not + * @throws if used outside of an xstate-tree root + */ +export function useOnRoute(route: AnyRoute): boolean { + const activeRouteEvents = useActiveRouteEvents(); + + if (!activeRouteEvents) { + throw new Error( + "useOnRoute must be used within a RoutingContext. Are you using it outside of an xstate-tree Root?" + ); + } + + return activeRouteEvents.some( + (activeRouteEvent) => + activeRouteEvent.type === route.event && + (activeRouteEvent.meta as SharedMeta)?.indexEvent === true + ); +} diff --git a/xstate-tree.api.md b/xstate-tree.api.md index 68360ef..dd72763 100644 --- a/xstate-tree.api.md +++ b/xstate-tree.api.md @@ -416,6 +416,9 @@ export function useActiveRouteEvents(): { // @public export function useIsRouteActive(...routes: AnyRoute[]): boolean; +// @public +export function useOnRoute(route: AnyRoute): boolean; + // @public export function useRouteArgsIfActive(route: TRoute): ArgumentsForRoute | undefined;