react-router v6 extending search params instead of replacing it
Asked Answered
W

10

33

Currently, doing setSearchParams(/* an object */) will replace the existing search params. If I would like to extend the search params instead of replacing them, e.g. adding a new param. What will be the most elegant way to achieve that?

I know I can modify the searchParams directly, then do something like setSearchParams(newSearchParamsToObject). Yet, since searchParams is not an object but a URLSearchParams. The code will be long and ugly.

Edit: Since some one appears don't understand what I have posted

Currently if we do something like this:

const [searchParams, setSearchParams] = useSearchParams({ a: 1 });
setSearchParams({ b: 2 });

It will replace the searchParams with ?b=2.

What I would like to achieve is adding b=2 to existing search params, i.e. the final search params should be ?a=1&b=2.

Wellman answered 13/12, 2021 at 4:19 Comment(0)
M
53

I suppose it would/should be trivial to set the search param value you want to add.

const [searchParams, setSearchParams] = useSearchParams();

...

searchParams.set(newParamKey, newParamValue);
setSearchParams(searchParams);

Edit react-router-v6-extending-search-params-instead-of-replacing-it

const [searchParams, setSearchParams] = useSearchParams();

const clickHandler = () => {
  searchParams.set("b", 2);
  setSearchParams(searchParams);
};

...

<button type="button" onClick={clickHandler}>
  Add "b=2" param
</button>

Introduced in [email protected] the setSearchParams now also takes a function, similar to the useState hook, to access the previous query params object to update from.

setSearchParams(searchParams => {
  searchParams.set("b", 3);
  return searchParams;
});

This is handy if you are using the setSearchParams in a useEffect hook as you won't need to list the current searchParams object as an external dependency.

Masson answered 13/12, 2021 at 5:48 Comment(4)
setSearchParams([...searchParams.entries(), ['b', 2]]); it will append the value and you will end up with ?b=2?b=2?b2 (if for example you change the paramas pressing a button for example)Fomentation
@Fomentation Ack, you're totally right. It works once the first time, but it will keep appending duplicate query params upon subsequent calls. Thanks for the heads up, I should have tested better.Masson
I wonder if this code, searchParams.set(name, value); setSearchParams(searchParams);, mutates searchParams before setting it. What implications could this have? We were taught not to mutate objects.Shamus
@IgorSukharev It would mutate the searchParams object. The implication would be that the updated searchParams value would be available within the closing scope. This is likely the impetus for introducing the "updater function" API so it works similarly to the React useState updater syntax.Masson
M
8

I was just doing something similar and this is what I found out.

As mentioned above, you can use searchParams.set, but also searchParams.append. The difference is, when using append, it will add new search param, even when it has the same name.

So for given search params ?a=123&b=456:

setSearchParams((prevParams) => {
  prevParams.append('b', "789");
});

// output -> "?a=123&b=456&b=789"

but if you use set the result is overwritten

setSearchParams((prevParams) => {
  prevParams.append('b', "789");
});

// output -> "?a=123&b=789"

But if you have more search params and you don't want to repeat set or append for each, you can use object spread with combination of entries() and Object.entries()

const newSearchParams = {
  a: 111,
  c: 000,
};

setSearchParams((prevParams) => {
  return new URLSearchParams({
    ...Object.fromEntries(prevParams.entries()),
    ...newSearchParams,
  });
});

// output -> "?a=111&b=456&c=000"
Mimeograph answered 16/3, 2023 at 7:54 Comment(0)
H
3

This version ->

setSearchParams(
    prev => ([...prev.entries(), ['foo', 'bar']])
);

will add the same param without replacing, like this ->

?s=5&p=0&p=1

param p was added the second time, instead of ?s=5&p=1

Higgs answered 6/12, 2022 at 15:55 Comment(0)
K
2

Router v6 solution here (don't know if works with previous version too): setSearchParams accept a function that should return new search params, actually giving you the previous value:

For example:

setSearchParams(prev => ([...prev.entries(), ['foo', 'bar']]);

This is basically like @Drew Reese answer, but using the previous "state", like you would do with useState from React.

Kicker answered 16/11, 2022 at 13:46 Comment(1)
I tried your solution and if the param already exists it will keep appending it appending e.g.: ?foo=bar&foo=barFloe
C
0

You could also use the URLSearchParams.append(name, value) method, in case you want several 'b'.

setSearchParams(searchParams.append('b', 2));

It's not like your modifying the state, here. You're just manipulating an URLSearchParams object. You don't have to worry about mutating it.

Cabob answered 30/3, 2022 at 14:26 Comment(1)
Argument of type 'void' is not assignable to parameter of type 'URLSearchParamsInit'.Indemnity
D
0

React Router v6 have the useSearchParams() method that return an array that has, as a first value, a way to get the actual search params, and as a second, a method to set new values, the return value type definition is this: [URLSearchParams, SetURLSearchParams] and you can find more on the official documentation.

You can see a full working example to solve your problem on this codesandbox.

And this is a wrap up:

import React, { useCallback, useMemo } from "react";
import { useSearchParams } from "react-router-dom";

function getRandomNumber() {
  return Math.floor(Math.random() * 1000) + 1;
}

function getAllParamsFromEntries(searchParamsEntity) {
  const finalValues = {};
  const entries = searchParamsEntity.entries();
  let isDone = false;

  while (!isDone) {
    const iteratorElement = entries.next();
    if (iteratorElement.value) {
      finalValues[iteratorElement.value[0]] = iteratorElement.value[1];
    }
    isDone = iteratorElement.done;
  }

  return finalValues;
}

const MainScreen = () => {
  const [searchParamsEntity, setSearchParams] = useSearchParams();

  const allSearchParams = useMemo(() => {
    return getAllParamsFromEntries(searchParamsEntity);
  }, [searchParamsEntity]);

  console.log(allSearchParams);

  const appendRandomSearchParam = useCallback(() => {
    setSearchParams((previousParams) => {
      const searchParams = getAllParamsFromEntries(previousParams);
      return {
        ...searchParams,
        [`randomParam${getRandomNumber()}`]: getRandomNumber()
      };
    });
  }, [setSearchParams]);

  return (
    <div>
      <p>Search params now: {JSON.stringify(allSearchParams, undefined, 2)}</p>
      <button onClick={appendRandomSearchParam}>Append</button>
    </div>
  );
};
Desireah answered 4/1, 2023 at 12:34 Comment(0)
H
0

We can manipulate searchParams as normal object and its values as arrays.

Adding query param. This works as append.

const addTag = (tag) => {
  setSearchParams((prev) => {
    return {tags: [...prev.getAll('tags'), tag]};
  });
}

Result

?tag=one
?tag=one&tag=two

Now if we want to delete some query param from this array.

const deleteTag = (tag) => {
  const includesTag = searchParams.getAll('tags').includes(tag);
  setSearchParams(prev => {
    if (includesTag) {
      const updatedTags = prev.getAll('tags').filter((el) => el !== tag);
      return { tags: updatedTags };
    }
  }
}
Healion answered 8/7, 2023 at 20:25 Comment(0)
N
0

Try this one:

const patchSearchParams = (newParams = {}) => {
  setSearchParams((searchParams) => {
    const prevParams = {};
    searchParams.forEach((value, key) => {
      prevParams[key] = value;
    });
    return { ...prevParams, ...newParams };
  });
};
Nehru answered 27/10, 2023 at 13:15 Comment(0)
G
0

Try this custom hook that wrap the useSearchParams() hook:

import {useSearchParams} from "react-router-dom";

const useSearchParamValue = (key, defaultValue) => {
   const [searchParams, setSearchParams] = useSearchParams();

   const value = searchParams.get(key) || defaultValue
   const setValue = (newValue) => {
      setSearchParams((prev) => {
         prev.set(key, newValue)
         return prev
      })
   }

   return [value, setValue]
}

export default useSearchParamValue

And this is how you should use it inside the component

const [myParam, setMyParam] = useSearchParamValue('myParam', 'defaultValue')
Geometrize answered 13/5, 2024 at 8:9 Comment(0)
H
0

Imho the best way to consume the useSearchParam API is by creating a new URLSearchParam object as is explained below:

  const [searchParams, setSearchParams] = useSearchParams();
  const updateSearchParams = useCallback(
    (newParam: string) =>
      setSearchParams((prevParams) => {
        const newParams = new URLSearchParams(prevParams);
        newParams.set("newParam", newParam);
        return newParams;
      }),
    [setSearchParams]
  );
Headrest answered 11/6, 2024 at 9:42 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.