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();
+});