diff --git a/packages/wouter/src/use-hash-location.js b/packages/wouter/src/use-hash-location.js index 562bc2d..eef59b5 100644 --- a/packages/wouter/src/use-hash-location.js +++ b/packages/wouter/src/use-hash-location.js @@ -19,8 +19,39 @@ const subscribeToHashUpdates = (callback) => { }; }; +export const useLocationProperty = (fn, ssrFn) => + useSyncExternalStore(subscribeToHashUpdates, fn, ssrFn); + +// when useHashLocation is used, location.search is always empty +// so we must retrieve string from the hash +const currentSearch = () => { + const hash = location.hash; + const hashLocation = "/" + hash.replace(/^#?\/?/, ""); + + const questionMarkIdx = hashLocation.indexOf("?"); + if (questionMarkIdx !== -1) { + return hashLocation.slice(questionMarkIdx + 1, hashLocation.length); + } + + return ""; +}; + +export const useSearch = ({ ssrSearch = "" } = {}) => + useLocationProperty(currentSearch, () => ssrSearch); + // leading '#' is ignored, leading '/' is optional -const currentHashLocation = () => "/" + location.hash.replace(/^#?\/?/, ""); +const currentHashLocation = () => { + const hash = location.hash; + const hashLocation = "/" + hash.replace(/^#?\/?/, ""); + + // remove query string + const questionMarkIdx = hashLocation.indexOf("?"); + if (questionMarkIdx !== -1) { + return hashLocation.slice(0, questionMarkIdx); + } + + return hashLocation; +}; export const navigate = (to, { state = null } = {}) => { // calling `replaceState` allows us to set the history diff --git a/packages/wouter/test/use-hash-location.test.tsx b/packages/wouter/test/use-hash-location.test.tsx index 77b2b0a..8da8052 100644 --- a/packages/wouter/test/use-hash-location.test.tsx +++ b/packages/wouter/test/use-hash-location.test.tsx @@ -3,7 +3,7 @@ import { renderHook, render } from "@testing-library/react"; import { renderToStaticMarkup } from "react-dom/server"; import { Router, Route, useLocation, Link } from "wouter"; -import { useHashLocation } from "wouter/use-hash-location"; +import { useHashLocation, useSearch } from "wouter/use-hash-location"; import { waitForHashChangeEvent } from "./test-utils"; import { ReactNode, useSyncExternalStore } from "react"; @@ -29,6 +29,14 @@ it("isn't sensitive to leading slash", () => { expect(path).toBe("/app/users"); }); +it("isn't sensitive to query string", () => { + location.hash = "/app/users?foo=bar"; + const { result } = renderHook(() => useHashLocation()); + const [path] = result.current; + + expect(path).toBe("/app/users"); +}); + it("rerenders when hash changes", async () => { const { result } = renderHook(() => useHashLocation()); @@ -196,3 +204,16 @@ it("defines a custom way of rendering link hrefs", () => { expect(getByTestId("link")).toHaveAttribute("href", "#/app"); }); + +it("useSearch returns correct query string when useHashLocation is used", () => { + location.hash = "/"; + const { result } = renderHook(() => useHashLocation()); + const [, navigate] = result.current; + + navigate("/?hello=world"); + const { + result: { current: search }, + } = renderHook(() => useSearch()); + + expect(search).toBe("hello=world"); +}); diff --git a/packages/wouter/types/use-browser-location.d.ts b/packages/wouter/types/use-browser-location.d.ts index 6485d76..d3b3ca3 100644 --- a/packages/wouter/types/use-browser-location.d.ts +++ b/packages/wouter/types/use-browser-location.d.ts @@ -1,6 +1,13 @@ import { Path, SearchString } from "./location-hook.js"; -type Primitive = string | number | bigint | boolean | null | undefined | symbol; +export type Primitive = + | string + | number + | bigint + | boolean + | null + | undefined + | symbol; export const useLocationProperty: ( fn: () => S, ssrFn?: () => S diff --git a/packages/wouter/types/use-hash-location.d.ts b/packages/wouter/types/use-hash-location.d.ts index e3573c5..b9d9d1f 100644 --- a/packages/wouter/types/use-hash-location.d.ts +++ b/packages/wouter/types/use-hash-location.d.ts @@ -1,7 +1,15 @@ import { Path } from "./location-hook.js"; +import { BrowserSearchHook, Primitive } from "./use-browser-location.js"; export function navigate(to: Path, options?: { state: S }): void; +export const useLocationProperty: ( + fn: () => S, + ssrFn?: () => S +) => S; + +export const useSearch: BrowserSearchHook; + export function useHashLocation(options?: { ssrPath?: Path; }): [Path, typeof navigate];