React router search parameters manipulation explained with examples

My struggles with URL parameters and the painful lessons I learned that hopefully make your life easier

May Chen
NEXL Engineering
5 min readAug 14, 2020

--

In case you don’t know what URL search parameter is, it looks like this

https://some.domain.com/search?country=AU&term=winsom

Basically it’s everything after ? and before # in a URL.

Why we started using URL search parameters?

We initially started using search params not to query the backend but to retain the search state in the frontend.

Imagine this scenario, you are on a search page (/search) and type in a keyword, a list of users shows up and you visit one of the user’s profile page(/user/user-a) and coming back to the search result, unfortunately the original search is gone and it can be quite frustrating if you have to type in the search or navigate to the 4th page again. That’s the problem we had on NEXL, because the search keyword and filters are stored in a state in <Search /> component under route /search, when you leave the route and come back, <Search /> gets unmounted and mounted so the state gets re-initiated.

So I created a context to store all the search states.

  const { search } = useLocation();
const params = queryString.parse(search, { parseNumbers: true, parseBooleans: true });
const [page, setPage] = useState<number>(typeof params.page === "number" ? params.page : 0); // page state
const [term, setTerm] = useState<string>(typeof params.term === "string" ? params.term : ""); // search keyword
const [fadingStatus, setFadingStatus] = useState<IFadingStatus | undefined>(
typeof params.status === "string" ? IFadingStatus[params.status] : undefined,
); // some filter

And push to URL params when there’s an update using useEffect hook.

useEffect(() => {
history.push({
search: `?${queryString.stringify({
page,
rowsPerPage,
term,
favourite,
opportunity,
status,
})}`,
});
}, [page, rowsPerPage, history, term, favourite, opportunity, fadingStatus]);

View full implementation here.

Note: I use a very handy package called query-string to parse URL params to object or stringify search states to URL params.

Mapping string from URL param to enum

A small issue I had here is we have a search filter which is an enum but it’s a string when parsed from URL parameters, so I have to map the string to enum when updating state on URL update.

In this case, we have to assign string values to our enum, like this

export enum IFadingStatus {
UnderMonth = 'UnderMonth',
OverMonth = 'OverMonth',
Over3Months = 'Over3Months',
Over6Months = 'Over6Months'
}

So we can find the exact enum by its string value like this

IFadingStatus.UnderMonth === IFadingStatus["UnderMonth"]

In real code, it looks like

const params = queryString.parse(search); // import queryString from "query-string"
const [fadingStatus, setFadingStatus] = useState<IFadingStatus | undefined>(
typeof params.status === "string" ? IFadingStatus[params.status] : undefined,
);

The URL looks soooooo long and ugly

Another minor issue is now whenever you go to the search page, the url always look like this even though all the params are either undefined or default value so we don’t actually need any of them.

/search?favourite=false&opportunity=false&page=0&rowsPerPage=25&status=&term=

So I did a little clean up in history.push function to only add the params that got updated

useEffect(() => {
history.push({
search: `?${queryString.stringify({
page: page ? page : undefined,
rowsPerPage: rowsPerPage === 25 ? undefined : rowsPerPage,
term: term ? term : undefined,
favourite: favourite ? favourite : undefined,
opportunity: opportunity ? opportunity : undefined,
status: fadingStatus ? fadingStatus : undefined,
})}`,
});
}, [page, rowsPerPage, history, term, favourite, opportunity, fadingStatus]);

With this, when you first land on the search page and haven’t done any action, the url is simply /search.

And say you updated search keyword, it looks nice and clean

/search?term=something

And you broswe the second page, it’s still pretty straightforward without throwing a long nonsense string at your face.

/search?term=something&page=1

Pushing to history on state update breaks browser back function

Not long after we implemented the solution above, we got a bug. When you land on search page and click on “back” on your browser, it doesn’t go back to the previous page until you click the second time, or the 6th time depending on how many search/filters you have done on the page. This is super annoying to our users or even to us working on the platform.

After some online search I found history.push() is to blame. So I updated the effect to use history.replace() so every search update doesn’t get pushed to the history stack.

useEffect(() => {
history.replace({
search: `?${queryString.stringify({
page: page ? page : undefined,
rowsPerPage: rowsPerPage === 25 ? undefined : rowsPerPage,
term: term ? term : undefined,
favourite: favourite ? favourite : undefined,
opportunity: opportunity ? opportunity : undefined,
status: fadingStatus ? fadingStatus : undefined,
})}`,
});
}, [page, rowsPerPage, history, term, favourite, opportunity, fadingStatus]);

One drawback of this is now if you navigate to the second page of the search result and click on go back, it goes back to the previous page before landing on the search page instead of the first page of the result. But it’s good enough for now and we’ll fix it when someone complains.

Other search params get overwrited on the search page

Soon another task came in and we wanted to do a product tour on the search page with a third party pluggin. The way we initiate the tour with the third party pluggin is to add a search param to the url like ?product_tour=true. So we have a getting started page for user to get familiar with how everything works on our platform, it has a link to the search page indicating this visit needs a product tour (/search?product_tour=true).

The problem is when you land on the search page product_tour=true always gets overwrited by the search state params before the third party pluggin could pick it up and the tour never happens.

The trick to fix this issue is to add the previous params in the new search object before stringifying it. And the previous params should be added before the search state params so the new search state value will overwrite the previous ones.

useEffect(() => {
history.replace({
search: `?${queryString.stringify({
...queryString.parse(history.location.search),
page: page ? page : undefined,
rowsPerPage: rowsPerPage === 25 ? undefined : rowsPerPage,
term: term ? term : undefined,
favourite: favourite ? favourite : undefined,
opportunity: opportunity ? opportunity : undefined,
status: fadingStatus ? fadingStatus : undefined,
})}`,
});
}, [page, rowsPerPage, history, term, favourite, opportunity, fadingStatus]);

Thanks for reading and I hope this helps your project. I still find the solutions and fixes here are quite clumsy, let me know if you have any suggestions to any points. I’d love to hear =)

--

--

May Chen
NEXL Engineering

A developer who occasionally has existential crisis and thinks if we are heading to the wrong direction, technology is just getting us there sooner.