molefrog / wouter

🥢 A minimalist-friendly ~2.1KB routing for React and Preact

Home Page:https://npm.im/wouter

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

`useSearchParams` for extracting and modifying search parameters

aaravk7 opened this issue · comments

Need something like useSearchParams in React Router to handle queries.

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.

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.

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

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

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

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.

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

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

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