From f451cba1e8ded0a1c4e08abefd1032088ebb01d8 Mon Sep 17 00:00:00 2001 From: John Ivan <71377638+ivanjohn121523@users.noreply.github.com> Date: Sat, 17 Oct 2026 17:38:20 +0000 Subject: [PATCH] Keep hash link queries before the hash Link hrefs for useHashLocation prefixed the whole target with #, so a query landed inside the fragment. Opening that address made the path /settings?tab=profile, which does not match /settings. Place the query before the hash, matching navigate(). --- packages/wouter/src/use-hash-location.js | 9 ++++- .../wouter/test/use-hash-location.test.tsx | 36 ++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/packages/wouter/src/use-hash-location.js b/packages/wouter/src/use-hash-location.js index 130b886b..79dee1b4 100644 --- a/packages/wouter/src/use-hash-location.js +++ b/packages/wouter/src/use-hash-location.js @@ -51,4 +51,11 @@ export const useHashLocation = ({ ssrPath = "/" } = {}) => [ navigate, ]; -useHashLocation.hrefs = (href) => "#" + href; +// Search stays before the hash, matching `navigate`. A query inside the hash +// (`#/settings?tab=1`) is not a separate search and fails route matching. +useHashLocation.hrefs = (href) => { + const queryAt = href.indexOf("?"); + return queryAt < 0 + ? "#" + href + : href.slice(queryAt) + "#" + href.slice(0, queryAt); +}; diff --git a/packages/wouter/test/use-hash-location.test.tsx b/packages/wouter/test/use-hash-location.test.tsx index 59856f5a..3b743eb4 100644 --- a/packages/wouter/test/use-hash-location.test.tsx +++ b/packages/wouter/test/use-hash-location.test.tsx @@ -2,7 +2,7 @@ import { test, expect, mock } from "bun:test"; import { renderHook, render, act, fireEvent } from "@testing-library/react"; import { renderToStaticMarkup } from "react-dom/server"; -import { Router, Route, useLocation, Link } from "../src/index.js"; +import { Router, Route, useLocation, useSearch, Link } from "../src/index.js"; import { useHashLocation } from "../src/use-hash-location.js"; import { waitForHashChangeEvent } from "./test-utils"; @@ -377,3 +377,37 @@ test("uses string URLs as hashchange event payload", () => { removeEventListener("hashchange", hashChangeFn); }); + +test("Link href keeps a query before the hash so opening it matches the route", () => { + history.replaceState(null, "", "/app#/home"); + + const { getByRole, unmount } = render( + + Settings + + ); + + const href = getByRole("link", { name: "Settings" }).getAttribute("href"); + expect(href).toBe("?tab=profile#/settings"); + unmount(); + + const opened = new URL(href!, "https://example.test/app"); + history.replaceState(null, "", opened.pathname + opened.search + opened.hash); + + const Settings = () => { + const search = useSearch(); + return <>Settings page {search}; + }; + + const { getByText, queryByText } = render( + + + + + Home + + ); + + expect(queryByText("Home")).not.toBeInTheDocument(); + expect(getByText("Settings page tab=profile")).toBeInTheDocument(); +});