react-router
react-router copied to clipboard
[Feature]: Take a SetStateAction in useSearchParams setter
What is the new or updated feature that you are suggesting?
It would be great if the setter function returned by useSearchParams
could accept a function that takes the current search params and builds a new one, like the setter returned by useState
.
const [params, setParams] = useSearchParams();
const setAbc = useCallback((value: string) => {
setParams((params) => {
params.set("abc", value);
return params;
});
}, [setParams]);
The input type of the first argument of the setter would be expanded from URLSearchParamsInit
to SetStateAction<URLSearchParamsInit>
.
The resulting function you get from the useCallback
in the example above now has a stable identity, and does not have to change as params
changes in order to preserve search params other than "abc"
.
Why should this feature be included?
I'm struggling to recreate functionality I had in v5 to implement a hook that manages the value of single search parameter rather than the entire set of search params, so different hooks and components can simultaneously manage parts of the search params without interfering with each other (and without having to reconstruct the rest of the search state when setting its parameter to a new value). A trimmed down implementation of this functionality in v5 looked like this:
function useSearchParam(key: string) {
const history = useHistory();
const { search } = useLocation();
const setter = useCallback((value) => {
// Use history's latest location in case other setters have been called in that same update.
const params = new URLSearchParams(history.location.search);
params.set(key, value);
history.push({ search: params.toString() });
}, [history, key]);
return [new URLSearchParams(search).get(key), setter];
}
The key part of this hook was the appearance of history.location.search
to construct an up to date view of the search params in the setter. Using this meant two things:
- The value of
search
does not need to be in the deps array ofuseCallback
, so the identity of the setter does not change when the location does. - Simultaneous changes to multiple different search parameters don't conflict with one another, because
history.location.search
is always up to date.
The second point is a crucial one: if a single event calls multiple setters of different useSearchParam
hooks, using the value of search
from useLocation
will mean each change gets reset by each subsequent setter, so only the last one will take effect. Using the mutable value of history.location.search
allows each change to be preserved in the changes that the following setters take.
I've managed to implement the same behaviour in v6 by fetching the history out of the context:
function useSearchParam(key: string) {
const [params, setParams] = useSearchParams();
const { navigator } = useContext(UNSAFE_NavigationContext);
const setter = useCallback((value) => {
// Use history's latest location in case other setters have been called in that same update.
const params = new URLSearchParams((navigator as History).location.search);
params.set(key, value);
setParams(params);
}, [navigator, setParams, key]);
return [new URLSearchParams(search).get(key), setter];
}
This relies on knowledge about the internals of the library that are intentionally not exposed, for good reason.
If setParams
can provide the current value of the search params when you call it, then both of these problems are solved: params
need not be in the deps array of a useCallback
in order to avoid touching other parameters, and if the given params always reflect previous changes in the same event then they will be preserved in the resulting transformations.
Would be awesome to merge #8344 I'm missing this functionality as well. Coming from v5 and using useQueryParam
this would be very much appreciated.
Do you have any news about this feature ?
Adapting @zmthy snippet, we can make it work using createSearchParams
:
function useSearchParam(key: string) {
const [search, setSearch] = useSearchParams();
const setter = useCallback((value) => {
const params = createSearchParams(search);
params.set(key, value);
setSearch(params);
}, [search, setSearch, key]);
return [createSearchParams(search).get(key), setter];
};
@zmthy My solution to this is:
setSearchParams({ ...Object.fromEntries([...searchParams]), paramToUpdate: "updatedValue", })
This spreads the object created from the searchParams key-value pairs into the new setSearchParams object. Note that Object.fromEntries is not IE compatible.
Thanks to #7586, you can now solve this problem with the unstable_HistoryRouter
, if you're prepared to use that instead of a BrowserRouter
. Follow the docs on how to set up the history router as a regular browser router, but with a history object that's under your control: https://reactrouter.com/docs/en/v6/api#unstable_historyrouter
You can then reference the history
definition, or put it in a context and recreate useHistory
with useContext
, though I would recommend bundling together the hooks that need access to the history object to reference the mutable location and avoid exporting the actual history object if possible.
Assuming the history
object is in scope, here's an implementation of the desired behaviour:
export function useSearchParamsUpdate(): [
URLSearchParams,
Dispatch<(params: URLSearchParams) => URLSearchParamsInit>
] {
const [params, setParams] = useSearchParams();
return [
params,
useCallback(
(update) =>
setParams(update(new URLSearchParams(history.location.search))),
[setParams]
),
];
}
This can be used to implement useSearchParam
with the desired properties, or useSearchParam
could be defined directly using the history
.
I'm not sure if it's reasonable to close the issue just yet, because it's inconvenient that you have to set up the router with your own managed history instead of using browser router, and the history router interface is still unstable. Ideally there would be a way to at least get a location ref with a stable identity and a mutable reference to the current location across any of the routers.
I'm going to convert this to a discussion so it can go through our new Open Development process. Please upvote the new Proposal if you'd like to see this considered!