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