Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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

[Feature]: Take a SetStateAction in useSearchParams setter #8287

Closed
zmthy opened this issue Nov 9, 2021 · 6 comments
Closed

[Feature]: Take a SetStateAction in useSearchParams setter #8287

zmthy opened this issue Nov 9, 2021 · 6 comments
Labels

Comments

@zmthy
Copy link

zmthy commented Nov 9, 2021

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:

  1. The value of search does not need to be in the deps array of useCallback, so the identity of the setter does not change when the location does.
  2. 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.

@jacobgavin
Copy link

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.

@ddecrulle
Copy link

Do you have any news about this feature ?

@lucassouza1
Copy link

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];
};

@davidperklin
Copy link

davidperklin commented Mar 28, 2022

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

@zmthy
Copy link
Author

zmthy commented May 11, 2022

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.

@brophdawg11
Copy link
Contributor

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!

@remix-run remix-run locked and limited conversation to collaborators Jan 9, 2023
@brophdawg11 brophdawg11 converted this issue into discussion #9845 Jan 9, 2023

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
Projects
None yet
Development

No branches or pull requests

6 participants