Handle Query Strings Like a Pro With React Router and Custom Hooks

useSearchParams

Luis Guerrero
bitso.engineering
7 min readSep 1, 2022

--

Photo by Markus Winkler on Unsplash

Intro

First thing first, probably you already know, but let’s lay the foundations.

What’s a query string?

It is the part of the URL that assigns values to specified parameters and although there is no universal standard, most of the implementations follow the next rules:

  • It is composed of a series of field-value pairs.
  • Within each pair, the field name and value are separated by an equals sign, =.
  • An ampersand separates the series of pairs, &.
  • Multiple values can be associated with a single field.

Let's start by analyzing the following URL:

http://example.com/?field1=value1&field1=value2&field2=value3

We can say that the URL above has 2 parameters, field1 and field2 the first has 2 associated values (value1 and value2) and the second has an associated value (value3).

By nature, a query string is open since the user can write whatever they want in the URL bar. So, it is up to the developers to convert the values into something meaningful for the implementation, which can present some challenges as we will see below.

Example

We are going to use the old reliable TODO list example to demonstrate how to handle some of the challenges we could face.

Image: SpongeBob SquarePants

User Story

As a user, I want to see a list of TODOs so I can organize my day.

Functional Requirements

  • The list should be paginated.
  • Users should be able to select the number of items they want to see on each page.
  • Page size options are fixed, possible values are 10, 15, and 20.
  • Users should be able to save the list state as a bookmark.

Technical Requirements

  • Use React Router.
  • Use Typescript.

Code Time

The last bullet of the functional requirements is the thing that we will be focusing on in this post. For the user to be able to save the state of the list as a bookmark we need to save the page and the page size in the URL. Something like this 👇

http://example.com/?page=1&size=15

Now, the best way to handle query strings in Javascript is using the URLSearchParms interface, which provides us with very helpful utility methods like get , getAll , has , append , delete , among others.

React Router exposes that interface through a custom hook called useSearchParams which returns a URLSearchParams object and a setter function.

Important: Even when the URLSearchParams interface has methods to add, update and delete values it doesn’t mean the URL will be updated when we change the object, that’s why we need the setter function.

👆 💻 With the above code, we have access to the query string using the searchParams variable and we can update the URL using the setSearchParams function.

Note: All code is in text format at the end of the post for you to copy if you wish..

And this is when the fun begins.

Image: Star Wars Revenge of the Sith

Since the user can put whatever they want in the URL we will need to parse it to handle unwanted values. Imagine the user writes the next URL 👇

http://example.com/?page=random&size=3.1416

Important: All the values we get from the query string using the URLSearchParams interface are of type string or an array of strings.

And also imagine (a lot of imagination I know) we have a List component that is expecting two numeric parameters page and size. 💻

The first thing we need to do is to convert the values we get from the query string to what the List component needs.

First the page parameter.

☝️ 💻 I’ll briefly explain what the code above is doing, although it is very simple:

  • We check if the page parameter exists.
  • If it does exist, we try to parse it as an integer.
  • If the parse returns us a valid number and it is greater than zero (we cannot have a page zero or a negative page) we pass that number to the list component.
  • If the parse returns us NaN or the number is less than 1, we pass 1 (our default value) to the list.

Note: We should also check for an upper limit which I will skip for sake of simplicity.

Now let’s do the same with the size parameter. 💻

We have almost the same steps as with the page parameter, with the only difference that instead of checking if the parameter is a positive number greater than zero we are checking if it is one of the valid options.

We now have covered the case for reading the page and the size parameters from the query string, but also we want to update the URL when the user changes these values using the UI.

Let’s add to our List component two callbacks for when the user navigates between pages or changes the page size value. 💻

Inside those callbacks, we will update the URL with the new values using the setter function. 💻

Since the values are programmatically controlled inside our List component, we only need to set them as they are in the URL, but we need to convert them to strings first.

Important: Remember that changing the searchParams object doesn't update the URL, we need to call setSearchParams and pass the updated object.

This is what the complete code would look like to handle those 2 parameters👇 💻

A lot of code for a simple task, right? and more important, somewhat hard to test 😧

Custom Hooks To The Rescue!

We cannot do much about the code itself, but what we can do is extract it to a custom hook so we don’t clutter the main component, and with that, we can test it, maintain it and extend it easier.

With just a few changes we can have the next custom useListSearchParams hook 👇 💻

And with that, the main component will change to only this 👇 💻

Much cleaner, right? And now we have a reusable hook that we can use for other views where we need the same implementation 😎

Testing

Testing our implementation became easier now since we can test the custom hook in an isolated way, to do that we will use the React Hooks Testing Library.

The main gotcha here is that our hook depends on useSearchParams, which we cannot access outside of a Router, also we need to attach to the URL the parameters we need, luckily React Router provides us with something to do exactly that: MemoryRouter and this is how we can use it 👇 💻

Extracted into its own component so we can use it across the tests, the component receives the page and size parameters which we attach to the URL using the initialEntries prop.

Finally, let’s see what a test would look like.

👆 💻 The test is asserting that when a valid value is written in the URL for the page parameter, then that same value is returned in the page variable.

Conclusion

Working with query strings is not straightforward as the user can put anything in the URL and it’s up to the developers to parse it and coerce the values into something that makes sense to the implementation, these conversions can clutter core components and can become more difficult to test, that’s where custom hooks come in handy as they will allow us to better maintain the code, implement our tests easily, and reuse it.

Final Thoughts

We could go further and implement a “superpowered” and type-safe version of the URLSearchParams interface that only allows us to use known parameters and that we can pass other types of values instead of only strings like in the example below. With that, we can provide a less prone to errors (typos in keys for example) interface, and a better DX. 👇 💻

And then we could use it like this 👇

I’ll explore this more deeply in another post.

And that’s it for now. Thanks for reading!

Gist

--

--

Luis Guerrero
bitso.engineering

Product-focused Software Engineer with more than 13 years of experience delivering top-quality software for world-class companies. React JS/TS advocate.