From 39a4be4d50fcf737692e2f04d8a7ad5979f65740 Mon Sep 17 00:00:00 2001 From: Taylor Lodge Date: Tue, 17 Sep 2024 10:25:54 +1200 Subject: [PATCH] feat(routing): useOnRoute hook useIsRouteActive does not tell you whether you are on the route or not, just that it's part of the active route events For when you need to know if you are on exactly a specific route (it's an index route/the end of the chain) use `useOnRoute` --- src/index.ts | 1 + src/routing/index.ts | 1 + src/routing/useOnRoute.spec.tsx | 109 ++++++++++++++++++++++++++++++++ src/routing/useOnRoute.tsx | 28 ++++++++ xstate-tree.api.md | 3 + 5 files changed, 142 insertions(+) create mode 100644 src/routing/useOnRoute.spec.tsx create mode 100644 src/routing/useOnRoute.tsx 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;