DEV Community

Cover image for React Hook to Manage URL Search Params Like a Boss:?mess=less
a-dev
a-dev

Posted on

React Hook to Manage URL Search Params Like a Boss:?mess=less

Keeping your page state in sync with URL search params doesn't seem like a big deal (it's definitely not cache invalidation). However, it becomes painful when you have multiple parameters, validation logic, and interdependencies πŸ™„ between parameters. I once faced this obstacle and created a hook that helped me to solve the problem. And now I want to share it with you. Yes, it's a React hook, yet the idea and DX can be adapted to other frameworks.

Before, I have to say that my react-hook is not a silver bullet. There are couple libraries that you can use, and I admit that they can suit your needs better. I recommend you to check them out:

  • nuqs - literally state manager for search params. If you want to put JSON in search params, this is your choice. It offers adapters for Next.js, React Router. And documentation is awesome.

  • tanstack router - not 'just lib', I know. But it's really great and I like that they saw the problem of search params and did some good stuff around it.

The demo

You can click buttons and enter text in the input field here (ignore the decorative landing page β€” I just had some fun with v0). Notice how the URL changes. If you refresh the page, the state is restored from the URL.
Also, check out the code on GitHub, especially the hook itself.

DX

First, create a Zod schema for the URL search parameters. Suppose you have a catalog page for clothes with filtering and pagination; a basic version of the schema might look like this:

const schema = z
  .object({
    page: z.string().default('1'),
    type: z.enum(['t-shirt', 'jeans', 'jacket']).default('t-shirt'),
    brand: z.array(z.string()).optional(),
    from: z.string().optional(),
    to: z.string().optional(),
  })

type Schema = z.infer<typeof schema>;
Enter fullscreen mode Exit fullscreen mode

Notice, I've added default values for the page and type parameters β€” it isn't the best practice (more caveats will be discussed later), but it serves well for an example. This means that even if a user visits the page without any search parameters in the URL, the hook will add them to the address. For instance, if you go to this demo link without a query, you will still see ?page=1 in the address bar.

I strictly recommend you add parameters to the link on the page, it helps to avoid unnecessary renders and redirects. But sometimes it's not easy and this small hack could be useful.

The second business logic action that you find in this type filters, is 'dependent changes' β€” when one parameter changes, another should be changed too. For example, if you change the type to 't-shirt', the brand should be reset to default. It's a common case, and that really why I created this hook.

const beforeUpdateCallback: BeforeUpdateCallback<Schema> = (updatedParams) => {
  if (Object.keys(updatedParams).includes('page')) {
    /*
     * If the user is paginating,
     * leave the rest of the search parameters unchanged.
     */
    return updatedParams;
  }

  if (Object.keys(updatedParams).some((key) => ['type'].includes(key))) {
    /*
     * When the user changes the clothes type,
     * the brand filter should be reset.
     */
    return {
      page: '1',
      brand: [],
      ...updatedParams,
    };
  }

  /*
  * For any other changes, we simply reset pagination to the first page.
  */
  return {
    page: '1',
    ...updatedParams,
  };
};
Enter fullscreen mode Exit fullscreen mode

This callback enhances the user experience by automatically resetting filters and preventing users from landing on non-existent pages when they change the 'type' filter.

The last step is to create and export the hooks:

const {
  URLSearchParamsProvider: ClothesFilterProvider,
  useUpdateURLSearchParams: useUpdateClothesFilter,
  useGetURLSearchParams: useGetClothesFilter,
} = createUrlSearchParams<Schema>({
  zodSchema,
  beforeUpdateCallback,
});

export { ClothesFilterProvider, useUpdateClothesFilter, useGetClothesFilter };
Enter fullscreen mode Exit fullscreen mode

Type note: don't forget to add types of schema. One of the coolest feature is types for your search params.

UX Note: Make sure to debounce your form state updates. If you run the demo locally, you can simulate a longer data-loading time by changing the delay in the clientLoader (for example, set it to 1000ms). It shows how a sluggish search input can feel without proper debouncing

Implementation

Just wrap your clothes page component with ClothesFilterProvider and use the hooks to get and update search params. Here's how:

<ClothesFilterProvider>
  <ClothesPage />
</ClothesFilterProvider>
Enter fullscreen mode Exit fullscreen mode
const ClothesPage: FC = () => {
  const { page, type, brand, from, to } = useGetClothesFilter ();
  const updateFilter = useUpdateClothesFilter();

  return (
    <div>
      <button onClick={() => updateFilter({ page: '2' })}>Go to page 2</button>
      <button onClick={() => updateFilter({ type: 'jeans' })}>Show jeans</button>
      <button onClick={() => updateFilter({ brand: ['nike', 'adidas'] })}>Show Nike and Adidas</button>
      {/* and so on */}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

As you can see, working with arrays in this hook is straightforward. Remember to properly handle array changes, such as filtering when removing items and preserving existing selections. Check out the demo for practical example.

How it's done

Pretty simple. You need methods for encoding and decoding search parameters, some logic for handling array parameters, and a method for updating parameters that will call the callback before any actions. That's it.

One important addition I made but never used is default parameters that can modify the URL on the page loading. For it I use the React useLayoutEffect hook to prevent unnecessary renders. However, as mentioned earlier, I don't consider it as the best practice.

Look more closely at the source code and docs (ai gen) and feel free to ask questions.

In this version, the hook uses react-router v7 (though nothing has changed for v6). I haven’t tried it with Next.js yet, but I suppose it will work if the search-params methods are updated accordingly.

Library?

For now, I do not plan to publish this hook as a library. However, if you think it should be... let me know in comments.

Good luck! πŸš€

Top comments (0)