Skip to content

Commit

Permalink
feat(routing): useOnRoute hook
Browse files Browse the repository at this point in the history
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`
  • Loading branch information
UberMouse committed Sep 16, 2024
1 parent 5fa967f commit 39a4be4
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export {
useRouteArgsIfActive,
useActiveRouteEvents,
TestRoutingContext,
useOnRoute,
} from "./routing";
export { loggingMetaOptions } from "./useService";
export { lazy } from "./lazy";
1 change: 1 addition & 0 deletions src/routing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export {
} from "./handleLocationChange";
export { useIsRouteActive } from "./useIsRouteActive";
export { useRouteArgsIfActive } from "./useRouteArgsIfActive";
export { useOnRoute } from "./useOnRoute";

export {
RoutingContext,
Expand Down
109 changes: 109 additions & 0 deletions src/routing/useOnRoute.spec.tsx
Original file line number Diff line number Diff line change
@@ -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<any>(), "/");
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 }) => (
<RoutingContext.Provider value={{ activeRouteEvents: { current: [] } }}>
{children}
</RoutingContext.Provider>
),
});

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 }) => (
<RoutingContext.Provider
value={{
activeRouteEvents: {
current: [
{
type: "foo",
meta: { indexEvent: false },
originalUrl: "",
params: {},
query: {},
},
],
},
}}
>
{children}
</RoutingContext.Provider>
),
});

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 }) => (
<RoutingContext.Provider
value={{
activeRouteEvents: {
current: [
{
type: "foo",
meta: { indexEvent: true },
originalUrl: "",
params: {},
query: {},
},
],
},
}}
>
{children}
</RoutingContext.Provider>
),
});

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 }) => (
<RoutingContext.Provider
value={{
activeRouteEvents: {
current: [
{
type: "bar",
meta: { indexEvent: true },
originalUrl: "",
params: {},
query: {},
},
],
},
}}
>
{children}
</RoutingContext.Provider>
),
});

expect(result.current).toBe(false);
});
});
28 changes: 28 additions & 0 deletions src/routing/useOnRoute.tsx
Original file line number Diff line number Diff line change
@@ -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
);
}
3 changes: 3 additions & 0 deletions xstate-tree.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<TRoute extends AnyRoute>(route: TRoute): ArgumentsForRoute<TRoute> | undefined;

Expand Down

0 comments on commit 39a4be4

Please sign in to comment.