BlogReactJS

Get and set query params with React Router (+ custom hook)

Written by Codemzy on June 26th, 2023

I created a custom hook to get and set query parameters in the URL query string using React Router and its location object. Here's how (and why) I built it, with all the code you need to use it.

Today I was re-building a search component with ReactJS for a team page. When you navigate to the team page, you would see everyone on your team. And you could search by name or some other filter.

The search worked, but once you clicked on a result (a team member) if you clicked the back button in the browser, you would go back to all results (rather than the filtered search results).

Which didn't feel right.

I'm going to talk more about building the search component in another blog post, for this post, all you need to know is - I solved this issue using query parameters.

I build a custom (reusable) hook so I can use it for any component that needs to interact with query params in the future.

Why use query params with React Router

Go search "query params" in Google and you will see in the address bar that a query string has been added to the URL.

https://www.google.co.uk/search?q=query+params

Anything after the ? is your query string - in this case q=query+params.

I've never really been a heavy user of query parameters in the web apps I've built. I learned to code when JavaScript frameworks (especially single-page applications like React) were getting popular, and I would handle state in Redux, React State or React Context.

But I'm starting the learn that query params are so useful, especially for actions like search, that might keep you on the same page but show different contexts depending on values the user inputs.

If the values are stored in the component state, when the user navigates away from the component, the previous values are gone. But if they are stored as key-value pairs in a query string on the URL, then going back in the browser can restore the previous values.

We just need to set and get the query parameters!

Setting query params

In React Router, location represents where the app is. We don't want to change the location path, but we do want to add query params to the URL. In React Router, this is at location.search.

You can use the useHistory hook to navigate, and this will store the query string in the browser history (so the browser back button works).

import React from 'react';
import { useHistory } from 'react-router-dom'

function SearchComponent(props) {
  const history = useHistory();
  
  // updates the query params
  function setQueryParams() {
    history.push({
      search: "?page=1&text=john"
    }); 
  };
};

That works great, but we don't want to hard code the query string "?page=1&text=john" since it needs to take real data. Ideally, I'd like to pass it an object of params and they get turned into a query string by magic (or code).

Now, you can use URLSearchParams for this, but it's not supported in Internet Explorer which some people do still use 🙄, so for peace of mind I created a function to get it done.

// creates a query string from query object
function createQueryString(queryObject = {}) {
  let queryString = Object.keys(queryObject)
    .filter((key) => queryObject[key] && !(Array.isArray(queryObject[key]) && !queryObject[key].length))
    .map((key) => {
      return Array.isArray(queryObject[key]) ? queryObject[key].map(item => `${encodeURIComponent(key)}=${encodeURIComponent(item)}`).join('&') : `${encodeURIComponent(key)}=${encodeURIComponent(queryObject[key])}`;
    }).join('&');
  return queryString ? `?${queryString}` : "";
};

It will .filter() out any empty values or arrays, and turn the rest into a query string.

Let's see it in action!

console.log(createQueryString({ page: 1, text: "john" }));
// "?page=1&text=john"

console.log(createQueryString({ page: 1, text: "" }));
// "?page=1"

console.log(createQueryString({ page: 1, text: "", department: [ "hr", "accounts" ] }));
// "?page=1&department=hr&department=accounts"

console.log(createQueryString({ page: 1, text: "", department: [] }));
// "?page=1"

We can pass an object to setQueryParams, and using the createQueryString function, turn it onto query params in the address bar. Nice!

// updates the query params
function setQueryParams(queryObj) {
  history.push({
    search: createQueryString(queryObj)
  }); 
};

Getting query params

Now we can use setQueryParams to set the query string, but our component will also need to get the query string, and turn it back into our query object, so when we navigate our component knows what values to use.

We can useLocation to do this, and get the search from the location object.

import React from 'react';
import { useLocation, useHistory } from 'react-router-dom'

function SearchComponent(props) {
  const history = useHistory();
  const { search } = useLocation(); // get the search (query string) from the location

  // check if existing search
  const queryParams = React.useMemo(() => search, [search]);

  //...
};

It's in useMemo() so the queryParams object only updates when location.search changes in useLocation(). Useful if the changing query parameters trigger API calls.

This gets us the query string e.g. "?page=1&text=john", but of course, we want to parse that back into an object.

Again, you could use URLSearchParams for this, but I created a custom function for my own use.

// turns query string back into an object
function queryStringToObject(queryString = "", options = {}) {
  let queryObject = {};
  queryString && decodeURIComponent(queryString.replace('?', '')).split('&').map((itemString) => {
    let [itemKey, itemValue] = itemString.split("=");
    if (options.hasOwnProperty(itemKey)) {
      if (!queryObject[itemKey] && Array.isArray(options[itemKey])) {
        queryObject[itemKey] = [];
      }
      Array.isArray(options[itemKey]) ? queryObject[itemKey].push(itemValue) : queryObject[itemKey] = typeof options[itemKey] === "number" ? parseInt(itemValue) : itemValue;}
  });
  return queryObject;
};

It works like this:

console.log(queryStringToObject("?page=1&department=hr&department=accounts", { page: 0, department: []}));

// {
//   page: 1, 
//   department: ['hr', 'accounts']
// }

The queryStringToObject function takes a query string and an options object that tells it the default values (if no values are in the query string) - this is needed so the function knows the type of value. Which is especially useful when we want to use arrays or number values.

For example, if there was only one department key-value pair in the query string, I would still want to get an array back, rather than a string, because that data needs to be an array.

console.log(queryStringToObject("?page=1&department=hr", { page: 0, department: []}));

// {
//   page: 1, 
//   department: ['hr']
// }

Now we can update the useMemo hook to use the queryStringToObject function.

const queryParams = React.useMemo(() => queryStringToObject(search, { page: 0, department: []}), [search]);

The useQueryParams custom hook

Now I can set and get query params, let's move this into a custom hook that can be used by any component in the future.

I call it useQueryParams.hook.js!

import React from 'react';
import { useLocation, useHistory } from 'react-router-dom'
// utils
import { createQueryString, queryStringToObject } from './helpers';

function useQueryParams(options) {
  const { search } = useLocation();
  const history = useHistory();

  // get query params
  const queryParams = React.useMemo(() => queryStringToObject(search, options), [search]);

  // updates the query params
  function setQueryParams(queryObj) {
    history.push({
      search: createQueryString(queryObj)
    }); 
  };

  return { queryParams, setQueryParams };
};

export default useQueryParams;

Don't forget to include the createQueryString and queryStringToObject functions, or import them from a different file like I have done above.

Now I can use the custom hook in any React component where I need to get or set the query params. Here's how that looks in my search component.

import React from 'react';
import { useQueryParams } from './hooks/useQueryParams.hook.js'

function SearchComponent(props) {
  const { queryParams, setQueryParams } = useQueryParams({ page: 0, department: [] });

  // api call
  React.useEffect(() => {
    searchAPI(queryParams).then((results) => {
    // do something with the search results
    }
  }, [queryParams]);
  
  // updates the query params
  function handleSearch({ title, department }) {
    setQueryParams({ 
      ...(title && { title }),
      ...(department && { department }),
    });
  };
};

Hopefully, this shows how the hook works. When handleSearch is called, e.g. from a search input by the user, a search object is created (in this example, with the keys title and/or department but any keys can be used as needed for your use case).

The values get added as the query string to the location, by setQueryParams, which will then update queryParams and trigger a fresh API call. You can replace that useEffect with a useQuery if you are using React Query to manage server state.