From 1be09b888100ef9755085315efdb2231d6ba4e06 Mon Sep 17 00:00:00 2001 From: Peter Hurst Date: Fri, 21 Aug 2026 15:41:29 +0100 Subject: [PATCH] =?UTF-8?q?feat(jarl-react):=20435=20=E2=80=94=20forward?= =?UTF-8?q?=20NavOptions=20through=20useNavigate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit useNavigate always pushed a new location even though the underlying route atom setter accepts NavOptions (replace). Accept an optional second argument and forward it, mirroring the atom's own setter shape, so a replace-navigation no longer needs to drop to useSetAtom(routeAtom). Ticket: 435 --- .../jarl-react/src/__tests__/hooks.test.tsx | 17 ++++++++++++++++- packages/jarl-react/src/hooks.ts | 7 ++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/jarl-react/src/__tests__/hooks.test.tsx b/packages/jarl-react/src/__tests__/hooks.test.tsx index b098536d..8ef8d152 100644 --- a/packages/jarl-react/src/__tests__/hooks.test.tsx +++ b/packages/jarl-react/src/__tests__/hooks.test.tsx @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach } from "vitest"; +import { describe, it, expect, beforeEach, vi } from "vitest"; import { render, screen, fireEvent } from "@testing-library/react"; import { rootAtom } from "jarl-atoms"; import { useRoute, useNavigate, useIsActive, useHref, useLink } from "../hooks"; @@ -51,6 +51,21 @@ describe("useNavigate", () => { expect(screen.getByTestId("match")).toHaveTextContent("true"); expect(window.location.pathname).toBe("/about"); }); + + it("forwards { replace: true } as a replace navigation", () => { + goTo("/"); + const pushSpy = vi.spyOn(window.history, "pushState"); + const replaceSpy = vi.spyOn(window.history, "replaceState"); + const Probe = () => { + const navigate = useNavigate(aboutAtom); + return ; + }; + render(); + fireEvent.click(screen.getByText("Go")); + expect(replaceSpy).toHaveBeenCalled(); + expect(pushSpy).not.toHaveBeenCalled(); + expect(window.location.pathname).toBe("/about"); + }); }); describe("useIsActive", () => { diff --git a/packages/jarl-react/src/hooks.ts b/packages/jarl-react/src/hooks.ts index 68a9ad40..caba6b4c 100644 --- a/packages/jarl-react/src/hooks.ts +++ b/packages/jarl-react/src/hooks.ts @@ -1,6 +1,6 @@ import { useCallback, useMemo } from "react"; import { useAtom, useAtomValue, useSetAtom } from "jotai"; -import { DefaultParams, RouteAtom } from "jarl-atoms"; +import { DefaultParams, NavOptions, RouteAtom } from "jarl-atoms"; import { isActive } from "./isActive"; // Re-export jotai's own primitive hooks. Per jotai convention (see @@ -21,11 +21,12 @@ export function useRoute(routeAtom: RouteAtom) { /** * Returns a stable `navigate` function bound to one route atom. Calling it with param values - * pushes a new location. + * pushes a new location; pass `{ replace: true }` as a second argument for a replace navigation, + * mirroring the route atom's own setter (`set(routeAtom, values, { replace: true })`). */ export function useNavigate(routeAtom: RouteAtom) { const setRoute = useSetAtom(routeAtom); - return useCallback((to: T) => setRoute(to), [setRoute]); + return useCallback((to: T, options?: NavOptions) => setRoute(to, options), [setRoute]); } /**