← home

useQueryParam

Here’s a straightforward implementation of a React hook that allows you to set, modify and remove URL query parameters. There is already a popular and high-quality implementation by Peter Beshai on GitHub. I was going to use that at first but about two-thirds of the way through reading its source code I decided that it was a little overengineered for my simple use case. Peter’s use-query-params offers React Router and Reach Router integrations, uses React.createContext internally to pass their history objects down to subscribing components, has dependencies on external packages and requires you to import and then specify types (numbers, strings, arrays, etc.) for any query parameter you specify. All I want to do is append the odd string to a URL. I’m happy to rely on simple browser APIs to do it for me. (Should I need to store more sophisticated data types than strings to a URL search string in the future, I guess I’ll just try my luck with JSON.stringify and JSON.parse.)

So I decided to start from scratch and roll my own hook. Considering that it ended up taking only around 30 lines of code, I think it was the right decision for my use case. Here’s the code:

import { useState } from 'react'

const handleParam = (key, value, options = {}) => {
  // Required for SSR. Do nothing if location object is not available.
  if (typeof location !== `undefined`) {
    // historyMethod: push or replace (https://developer.mozilla.org/docs/Web/API/History)
    // to either add to the browser history or replace the last item
    const { historyMethod = `replace`, nullDeletes = true } = options

    // Parse current query string using the browser's URLSearchParams API.
    const params = new URLSearchParams(location.search)

    // If the passed value is undefined, check if the URL already contains
    // a value for it. This is important on initial page load.
    if (value === undefined) value = params.get(key)
    // If the passed value is null and the nullDeletes option is
    // set to true, delete the corresponding query parameter.
    else if (value === null && nullDeletes) params.delete(key)
    // Else use the provided key and value to set a new query parameter.
    else params.set(key, value)

    // Construct URL containing the updated query parameter(s).
    let target = location.pathname + `?` + params.toString()
    target = target.replace(//??$/, ``) // remove ? if search string is empty

    history[historyMethod + `State`]({ path: value }, ``, target) // update the browser URL

    return value
  }
}

export const useQueryParam = (key, value, options) => {
  // Relies on useState to trigger component rerenders on calls to setParam.
  const [param, setParam] = useState(handleParam(key, value, options))

  // override allows changing options for individual setQueryParam calls
  const setter = (newValue, override) =>
    setParam(handleParam(key, newValue, { ...options, ...override }))

  return [param, setter]
}

key and value will appear as ?(...&)key=value in the URL query. The options object can contain a boolean nullDeletes which specifies on a per-hook basis whether passing null as the value should delete the parameter from the URL query, as well as a string historyMethod that determines whether changes to the query should replace or be appended to the browser history. With the default historyMethod = 'replace', the user will get back to the previous page immediately when using the browser’s back button. historyMethod = 'push', will require one back button press for every time setQueryParam is called on that page.

Usage Example

Here’s an example of how to use this hook to filter a list of posts on a blog page by tag.

import React from 'react'
import { PostList, TagList } from 'components'
import { useQueryParam } from 'hooks' // highlight-line

const filterPostsByTag = (tag, posts) =>
  posts.filter(post => post.tags.includes(tag))

export default function BlogPage({ posts, tags }) {
  const [activeTag, setActiveTag] = useQueryParam(`tag`) // highlight-line
  const filteredPosts = filterPostsByTag(activeTag, posts)
  return (
    <>
      <TagList {...{ tags, activeTag, setActiveTag }} />
      <PostList posts={filteredPosts} />
    </>
  )
}

The TagList component in the last highlighted line simply uses setActiveTag in an onClick callback function onClick={() => setActiveTag(tag.title)}. Note that an All tag would set the active tag to null so that clicking it will remove the tag query parameter from the address bar completely.

import React from 'react'
import { Tag, TagGrid, Toggle } from './styles'

export const TagList = ({ tags, activeTag = `All`, setActiveTag }) => (
  <TagGrid>
    <h2>Tags</h2>
    {tags.map(({ title, count }) => (
        <Tag
          key={title}
          active={activeTag === title || (title === `All` && !activeTag)} // highlight-line
          onClick={() => setActiveTag(title === `All` ? null : title)} // highlight-line
        >
          {title} ({count})
        </Tag>
      )
    })
  </TagGrid>
)