Search code examples
javascriptreactjsreact-routerreact-router-dom

How to properly serialize query params?


In order to retrieve the current query params, I'm using this:

import { useLocation } from 'react-router-dom';

function useQuery() {
    return new URLSearchParams(useLocation().search);
}

Then in the functional component:

const query = useQuery();

However, I didn't find any integrated solution to easily set Link to the same query params except one with a new value.

Here is my solution so far:

const filterLink = query => param => value => {
  const clone = new URLSearchParams(query.toString());
  clone.set(param, value);
  return `?${clone.toString()}`;
}

return (
  <>
    <Link to={filterLink(query)('some-filter')('false')}>False</Link>
    <Link to={filterLink(query)('some-filter')('true')}>True</Link>
  </>
)

I have to clone the query object in order not to mutate the original one and have unwanted side effects when calling multiple times filterLink in the JSX. I also have to add the question mark myself because URLSearchParams.prototype.toString() does not add it.

I am wondering why I have to do that myself? I don't really like to do so low-level things when using a framework. Did I miss something in react-router? Is there a more common practice to do what I need?


Solution

  • Angular is a framework while React is generally considered to still be a library. As such, React is a little more Do It Yourself (DIY). React doesn't care about the URL queryString, and react-router is mainly interested in the URL path for route matching and rendering.

    However, react-router-dom@6 introduced a few new hooks and utility functions to help work with the queryString parameters. One utility that you may find helpful in this endeavor is the createSearchParams function.

    declare function createSearchParams(
      init?: URLSearchParamsInit
    ): URLSearchParams;
    

    createSearchParams is a thin wrapper around new URLSearchParams(init) that adds support for using objects with array values. This is the same function that useSearchParams uses internally for creating URLSearchParams objects from URLSearchParamsInit values.

    Based on your question and the other answers here I'm assuming you don't simply need to blow away previously existing search params and replace them with the current link, but rather that you want to possibly conditionally merge new parameters with any existing params.

    Create a utility function that takes the same props as the Link component's to prop (string | Partial<Path>). It's the partial Path types we care about and want to override.

    import { createSearchParams, To } from "react-router-dom";
    
    interface CreatePath {
      pathname: string;
      search?: {
        [key: string]: string | number;
      };
      hash?: string;
      merge?: boolean;
    }
    
    const createPath = ({
      hash,
      merge = true,
      pathname,
      search = {}
    }: CreatePath): To => {
      const searchParams = createSearchParams({
        ...(merge
          ? (Object.fromEntries(createSearchParams(window.location.search)) as {})
          : {}),
        ...search
      });
    
      return { pathname, search: searchParams.toString(), hash };
    };
    

    Usage:

    1. Link that merges params with existing queryString params:

      <Link to={createPath({ pathname: "/somePage", search: { a: 1 } })}>
        Some Page
      </Link>
      
    2. Link that replaces existing params:

      <Link
        to={createPath({ pathname: "/somePage", search: { b: 2 }, merge: false })}
      >
        Some Page
      </Link>
      

    I'd advise to take this a step further and create a custom Link component that does path creation step for you.

    Example building on the above utility function:

    import { Link as LinkBase, LinkProps as BaseLinkProps } from "react-router-dom";
    
    // Override the to prop
    interface LinkProps extends Omit<BaseLinkProps, "to"> {
      to: CreatePath;
    }
    
    // Use our new createPath utility
    const Link = ({ to, ...props }: LinkProps) => (
      <LinkBase to={createPath(to)} {...props} />
    );
    

    Usage is same as above but now the to prop is directly passed:

    1. Link that merges params with existing queryString params:

      <Link to={{ pathname: "/somePage", search: { a: 1 } }}>
        Some Page
      </Link>
      
    2. Link that replaces existing params:

      <Link to={{ pathname: "/somePage", search: { b: 2 }, merge: false }}>
        Some Page
      </Link>
      

    Demo:

    Edit how-to-properly-serialize-query-params