DEV Community

Jacob Gavin
Jacob Gavin

Posted on

React router v6 useSearchParams

How I did a more powerfull useSearchParams hook

With react router v5 I was using a library called use-query-params.
It had this great hook called useQueryParam which let you manage 1 query parameter in the same way as useState worked.

One great thing about useQueryParam is that it respects all other queries that you currently store in the url.
Meaning it only updates the value that you set out each hook the be responsible for

function SearchInput() {
    const [searchTerm, setSearchTerm] = useQueryParam('q', StringParam);

    return <input onChange={(event) => setSearchTerm(event.target.value)} value={searchTerm} />
}
Enter fullscreen mode Exit fullscreen mode

If you then had other components that updated other url parameters like filters and so on it still kept my "q"-parameter intact.

In React router v6 they expose a hook called useSearchParams which is great and it was really missing something like that from v5.
The only problem (that I think) is the fact that it overrides all other url parameters so you constantly have to have the entire url param object to update it with. But I want to have different component handling different parts of the url parameters.
That's why I wrote a new hook with inspiration from the use-query-param library.

I posted the entire hook down below. I utilized serialize-query-param library which is authored by the same person who wrote use-query-params. The hook I wrote works in the same way as useState.

function SearchInput() {
    const [searchTerm, setSearchTerm] = useSearchParam('q', StringParam);

    const changeSearchTerm = (event: React.ChangeEvent<HTMLInputElement>): void => {
        setSearchTerm(event.target.value, 'replace');
        // you could also use a callback function to set the value like this
        setSearchTerm((oldValue) => {
            // do something with oldValue if you like

            return event.target.value;
        }, 'replace') // replace or push to url (push is default)
    }

    return <input onChange={} value={searchTerm} />
}
Enter fullscreen mode Exit fullscreen mode

This is the end result of the hook I wrote. It's pretty straight forward. Unfortonatly I'm using UNSAFE_NavigationContext from react router. As far as I can tell it's okayish to use it. There are some issues on the react-router repo discussing this but as of writing this they
are probably not going to export a hook that can do what I want since they want to keep react-router lightweight but hopefully in the future they will expose this context in a more friendly way.

If you are using it in production, make sure to test it well.

import { isString } from 'lodash';
import { useContext } from 'react';
import { UNSAFE_NavigationContext, useSearchParams } from 'react-router-dom';
import { QueryParamConfig, StringParam } from 'serialize-query-params';

type NewValueType<D> = D | ((latestValue: D) => D);
type UrlUpdateType = 'replace' | 'push' | undefined;
type UseSearchParam<D, D2 = D> = [D2, (newValue: NewValueType<D>, updateType?: UrlUpdateType) => void];

export default function useSearchParam<D, D2 = D>(
    name: string,
    config: QueryParamConfig<D, D2> = StringParam as QueryParamConfig<any>,
): UseSearchParam<D, D2> {
    const [searchParams, setSearchParams] = useSearchParams();
    const { navigator } = useContext(UNSAFE_NavigationContext);

    const setNewValue = (valueOrFn: NewValueType<D>, updateType?: UrlUpdateType): void => {
        let newValue;
        const value = searchParams.get(name);
        if (typeof valueOrFn === 'function') {
            // eslint-disable-next-line @typescript-eslint/ban-types
            newValue = (valueOrFn as Function)(config.decode(value));
        } else {
            newValue = valueOrFn;
        }
        const encodedValue = config.encode(newValue);

        const params = new URLSearchParams((navigator as any).location.search);

        if (isString(encodedValue)) {
            params.set(name, encodedValue);
        } else {
            params.delete(name);
        }
        setSearchParams(params, { replace: updateType === 'replace' });
    };

    const decodedValue = config.decode(searchParams.get(name));
    return [decodedValue, setNewValue];
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)