Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

useSearchParams for extracting and modifying search parameters #368

Open
aaravk7 opened this issue Oct 20, 2023 · 10 comments · May be fixed by #391
Open

useSearchParams for extracting and modifying search parameters #368

aaravk7 opened this issue Oct 20, 2023 · 10 comments · May be fixed by #391
Labels
feature V3 Features that won't be released in v2, but planned for the next major release

Comments

@aaravk7
Copy link

aaravk7 commented Oct 20, 2023

Need something like useSearchParams in React Router to handle queries.

@AkshatGupta01
Copy link

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.

@molefrog
Copy link
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.

@aaravk7
Copy link
Author

aaravk7 commented Oct 23, 2023

@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.

@molefrog
Copy link
Owner

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

@molefrog molefrog reopened this Oct 23, 2023
@junwen-k
Copy link

junwen-k commented Nov 13, 2023

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 :)

@molefrog
Copy link
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.

@molefrog molefrog added the V3 Features that won't be released in v2, but planned for the next major release label Nov 14, 2023
@molefrog
Copy link
Owner

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

@molefrog molefrog changed the title Need support for search params. useSearchParams for extracting search parameters Nov 22, 2023
@molefrog molefrog changed the title useSearchParams for extracting search parameters useSearchParams for extracting and modifying search parameters Nov 22, 2023
@junwen-k junwen-k linked a pull request Nov 30, 2023 that will close this issue
@junwen-k
Copy link

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

@alzalabany
Copy link

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}`);
  }
}

@junwen-k
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature V3 Features that won't be released in v2, but planned for the next major release
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants