Skip to content

useSearchParams for extracting and modifying search parameters #368

Description

@aaravk7

Need something like useSearchParams in React Router to handle queries.

Activity

  1. AkshatGupta01 commented on Oct 20, 2023

    @AkshatGupta01

    I was also facing the issue of extracting search params. But I have built a custom hook for that. I can raise a pull request for it.

  2. molefrog commented on Oct 23, 2023

    @molefrog
    Owner

    Hi! Sorry, this isn't well documented (yet! PRs are welcome). You can subscribe to search string updates via:

    import { useSearch, useLocationProperty, navigate } from 'wouter/use-location';
    
    // get all search params:
    const search = useSearch();

    Note that you will have to parse these using URLSearchParams or a 3rd party library. We might add this in the future releases.

  3. aaravk7 commented on Oct 23, 2023

    @aaravk7
    Author

    @molefrog Actually the issue isn't getting the search params, it's actually updating them. We need a hook like useSearchParams of React Router Dom, to retrieve as well as update the search params.

  4. molefrog commented on Oct 23, 2023

    @molefrog
    Owner

    I see, this isn't provided out-of-the-box right now. But I'll try to hack a simple implementation for that.

  5. junwen-k commented on Nov 13, 2023

    @junwen-k

    I've written the useSearchParams function based on react-router's implementation for wouter. The API is similar if not identical to react-router's useSearchParams version.

    So far it has been working great for me, hope it helps :)

    lib/wouter.ts

    import { useCallback, useMemo, useRef } from 'react';
    import { navigate, useSearch } from 'wouter/use-location';
    
    // Based on react-router: https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/index.tsx
    
    type ParamKeyValuePair = [string, string];
    
    type URLSearchParamsInit =
      | string
      | ParamKeyValuePair[]
      | Record<string, string | string[]>
      | URLSearchParams;
    
    export function createSearchParams(
      init: URLSearchParamsInit = '',
    ): URLSearchParams {
      return new URLSearchParams(
        typeof init === 'string' ||
        Array.isArray(init) ||
        init instanceof URLSearchParams
          ? init
          : Object.keys(init).reduce((memo, key) => {
              const value = init[key];
              return memo.concat(
                Array.isArray(value) ? value.map((v) => [key, v]) : [[key, value]],
              );
            }, [] as ParamKeyValuePair[]),
      );
    }
    
    export function getSearchParamsForLocation(
      locationSearch: string,
      defaultSearchParams: URLSearchParams | null,
    ) {
      const searchParams = createSearchParams(locationSearch);
    
      if (defaultSearchParams) {
        // Use `defaultSearchParams.forEach(...)` here instead of iterating of
        // `defaultSearchParams.keys()` to work-around a bug in Firefox related to
        // web extensions. Relevant Bugzilla tickets:
        // https://bugzilla.mozilla.org/show_bug.cgi?id=1414602
        // https://bugzilla.mozilla.org/show_bug.cgi?id=1023984
        defaultSearchParams.forEach((_, key) => {
          if (!searchParams.has(key)) {
            defaultSearchParams.getAll(key).forEach((value) => {
              searchParams.append(key, value);
            });
          }
        });
      }
    
      return searchParams;
    }
    
    export function useSearchParams(defaultInit?: URLSearchParamsInit) {
      if (typeof URLSearchParams === 'undefined') {
        console.warn(
          `You cannot use the \`useSearchParams\` hook in a browser that does not ` +
            `support the URLSearchParams API. If you need to support Internet ` +
            `Explorer 11, we recommend you load a polyfill such as ` +
            `https://github.com/ungap/url-search-params\n\n` +
            `If you're unsure how to load polyfills, we recommend you check out ` +
            `https://polyfill.io/v3/ which provides some recommendations about how ` +
            `to load polyfills only for users that need them, instead of for every ` +
            `user.`,
        );
      }
    
      const defaultSearchParamsRef = useRef(createSearchParams(defaultInit));
      const hasSetSearchParamsRef = useRef(false);
    
      const search = useSearch();
      const searchParams = useMemo(
        () =>
          // Only merge in the defaults if we haven't yet called setSearchParams.
          // Once we call that we want those to take precedence, otherwise you can't
          // remove a param with setSearchParams({}) if it has an initial value
          getSearchParamsForLocation(
            search,
            hasSetSearchParamsRef.current ? null : defaultSearchParamsRef.current,
          ),
        [search],
      );
    
      const setSearchParams = useCallback(
        (
          nextInit:
            | URLSearchParamsInit
            | ((prev: URLSearchParams) => URLSearchParamsInit),
          navigateOpts?: Parameters<typeof navigate>['1'],
        ) => {
          const newSearchParams = createSearchParams(
            typeof nextInit === 'function' ? nextInit(searchParams) : nextInit,
          );
          hasSetSearchParamsRef.current = true;
          navigate('?' + newSearchParams, navigateOpts);
        },
        [searchParams],
      );
    
      return [searchParams, setSearchParams] as const;
    }

    app.tsx

    export function App() {
      const [searchParams, setSearchParams] = useSearchParams();
    
      return (
        <main>
          <p>{searchParams.get('key')}</p>
    
          <button onClick={() => setSearchParams({ key: 'value' })}>
            Set Param
          </button>
          <button
            onClick={() =>
              setSearchParams((prevSearchParams) => ({
                ...Object.fromEntries(prevSearchParams),
                array: ['value_1', 'value_2'], // Array is supported just like react-router.
              }))
            }
          >
            Merge Params
          </button>
        </main>
      );
    }

    If interested, I could open up a PR to add this as an official helper util :)

  6. molefrog commented on Nov 14, 2023

    @molefrog
    Owner

    If interested, I could open up a PR to add this as an official helper util :)

    Cool, that could be a nice feature to have in v3. We're preparing the first release candidate right now, I will let you know once it is ready, so you can start working on a PR.

  7. added
    V3Features that won't be released in v2, but planned for the next major release
    on Nov 14, 2023
  8. molefrog commented on Nov 22, 2023

    @molefrog
    Owner

    The RC is out now. There is no useSearchParams yet, but we added it to the roadmap for v3.1

  9. changed the title [-]Need support for search params.[/-] [+]`useSearchParams` for extracting search parameters[/+] on Nov 22, 2023
  10. changed the title [-]`useSearchParams` for extracting search parameters[/-] [+]`useSearchParams` for extracting and modifying search parameters[/+] on Nov 22, 2023
  11. junwen-k commented on Nov 30, 2023

    @junwen-k

    I've created a PR for this feature based on my understanding (I am no expert in Routing 👀).
    Any feedbacks are welcomed! @molefrog

  12. alzalabany commented on Jan 10, 2024

    @alzalabany

    i find it quite often that you might want to replace searchParam but you don't really want to trigger a re-render in same component!

    useSearch will trigger re-render everytime; infact current implementaion trigger re-render even if params/href didnot change at all. (it re-render on any navigation event)

    a nice js helper to manipulate searchParams is more than enough more most cases imo

    export function replaceSearchParam(qs: Record<string,string|number|boolean|null>, replace=false){
      const params = new URLSearchParams(location.search);
      for (const [key, value] of Object.entries(qs)) {
        if(value === null){
          params.delete(key);
        } else {
          params.set(key, `${value}`);
        }
      }
      if(replace){
        history.replaceState(null, "", `${location.pathname}?${params}`);
      } else {
        history.pushState(null, "", `${location.pathname}?${params}`);
      }
    }
    
  13. junwen-k commented on Jan 10, 2024

    @junwen-k

    i find it quite often that you might want to replace searchParam but you don't really want to trigger a re-render in same component!

    useSearch will trigger re-render everytime; infact current implementaion trigger re-render even if params/href didnot change at all. (it re-render on any navigation event)

    a nice js helper to manipulate searchParams is more than enough more most cases imo

    export function replaceSearchParam(qs: Record<string,string|number|boolean|null>, replace=false){
      const params = new URLSearchParams(location.search);
      for (const [key, value] of Object.entries(qs)) {
        if(value === null){
          params.delete(key);
        } else {
          params.set(key, `${value}`);
        }
      }
      if(replace){
        history.replaceState(null, "", `${location.pathname}?${params}`);
      } else {
        history.pushState(null, "", `${location.pathname}?${params}`);
      }
    }
    

    I believe triggering a re-render upon search changes is the desired behaviour. E.g, updating query ?status=failed will trigger an update on the UI and refetch API with status = "failed".

    May I know on what occasion that you would want to update the search params without re-rendering? This also mean that when you hit back on the browser, nothing happens because UI does not re-render.

  14. removed
    V3Features that won't be released in v2, but planned for the next major release
    on May 19, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions