Skip to content

Commit

Permalink
feat(useSearchParams): new hook
Browse files Browse the repository at this point in the history
  • Loading branch information
guoyunhe authored and molefrog committed Jan 25, 2025
1 parent ff2a7da commit ae5db0e
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/wouter-preact/src/preact-deps.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export {
Fragment,
} from "preact";
export {
useMemo,
useRef,
useLayoutEffect as useIsomorphicLayoutEffect,
useLayoutEffect as useInsertionEffect,
Expand Down
20 changes: 20 additions & 0 deletions packages/wouter/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
forwardRef,
useIsomorphicLayoutEffect,
useEvent,
useMemo,
} from "./react-deps.js";
import { absolutePath, relativePath, sanitizeSearch } from "./paths.js";

Expand Down Expand Up @@ -202,6 +203,25 @@ const useCachedParams = (value) => {
return (prev.current = curr);
};

export function useSearchParams() {
const [location, navigate] = useLocation();

const search = useSearch();
const searchParams = useMemo(() => new URLSearchParams(search), [search]);

// cached value before next render, so you can call setSearchParams multiple times
let tempSearchParams = searchParams;

const setSearchParams = useEvent((nextInit, options) => {
tempSearchParams = new URLSearchParams(
typeof nextInit === 'function' ? nextInit(tempSearchParams) : nextInit,
);
navigate(location + '?' + tempSearchParams, options);
})

return [searchParams, setSearchParams];
}

export const Route = ({ path, nest, match, ...renderProps }) => {
const router = useRouter();
const [location] = useLocationFromRouter(router);
Expand Down
1 change: 1 addition & 0 deletions packages/wouter/src/react-deps.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as React from "react";
const useBuiltinInsertionEffect = React["useInsertion" + "Effect"];

export {
useMemo,
useRef,
useState,
useContext,
Expand Down
60 changes: 60 additions & 0 deletions packages/wouter/test/use-search-params.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { renderHook, act } from "@testing-library/react";
import { useSearchParams, Router } from "wouter";
import { navigate } from "wouter/use-browser-location";
import { it, expect, beforeEach } from "vitest";

beforeEach(() => history.replaceState(null, "", "/"));

it("can return browser search params", () => {
history.replaceState(null, "", "/users?active=true");
const { result } = renderHook(() => useSearchParams());

expect(result.current[0].get('active')).toBe("true");
});

it("can change browser search params", () => {
history.replaceState(null, "", "/users?active=true");
const { result } = renderHook(() => useSearchParams());

expect(result.current[0].get('active')).toBe("true");

act(() => result.current[1](prev => {
prev.set('active', 'false');
return prev;
}));

expect(result.current[0].get('active')).toBe("false");
});

it("can be customized in the Router", () => {
const customSearchHook = ({ customOption = "unused" }) => "none";

const { result } = renderHook(() => useSearchParams(), {
wrapper: (props) => {
return <Router searchHook={customSearchHook}>{props.children}</Router>;
},
});

expect(Array.from(result.current[0].keys())).toEqual(["none"]);
});

it("unescapes search string", () => {
const { result: searchResult } = renderHook(() => useSearchParams());

expect(Array.from(searchResult.current[0].keys()).length).toBe(0);

act(() => navigate("/?nonce=not Found&country=საქართველო"));
expect(searchResult.current[0].get('nonce')).toBe("not Found");
expect(searchResult.current[0].get('country')).toBe("საქართველო");

// question marks
act(() => navigate("/?вопрос=как дела?"));
expect(searchResult.current[0].get('вопрос')).toBe("как дела?");
});

it("is safe against parameter injection", () => {
history.replaceState(null, "", "/?search=foo%26parameter_injection%3Dbar");
const { result } = renderHook(() => useSearchParams());

expect(result.current[0].get('search')).toBe("foo&parameter_injection=bar");
});
8 changes: 8 additions & 0 deletions packages/wouter/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,14 @@ export function useSearch<
H extends BaseSearchHook = BrowserSearchHook
>(): ReturnType<H>;

export type URLSearchParamsInit = ConstructorParameters<typeof URLSearchParams>[0];
export type SetSearchParams = (
nextInit: URLSearchParamsInit | ((prev: URLSearchParams) => URLSearchParamsInit),
options?: { replace?: boolean; state?: any },
) => void;

export function useSearchParams(): [URLSearchParams, SetSearchParams];

export function useParams<T = undefined>(): T extends string
? StringRouteParams<T>
: T extends undefined
Expand Down

0 comments on commit ae5db0e

Please sign in to comment.