How to Maintain React State in a URL Query String for a Shareable Application State
Tags: react, nextjs, typescript
Published: 2023-10-07 05:06:05

{% embed https://youtu.be/fLx-JvNa450 %}

🐙 [GitHub](https://github.com/radzionc/reactkit)  |  🎮 [Demo](https://georgiancitizen.com)

# Introduction to Maintaining React State in a URL Query String

In this post, I'll share with you an invaluable hook for maintaining React state in a URL query string. This comes in particularly handy when you wish to make your application state shareable via a URL. I'll be showcasing a practical example today, a pagination parameter for my NextJS app. I'm currently developing an application that includes a list of exam tickets, and I want to enable the sharing of a specific ticket from this list via a direct link. The reusable components and hooks discussed in this video can all be found in the [ReactKit repository on GitHub](https://github.com/radzionc/reactkit).

## How to Utilize the Hook in an App

Here's how to utilize the hook in an app:

```tsx
const [ticket, setTicket] = useQueryParamState<number>("ticket", 1)
```

## Supporting Various Types of Values

We feed the name of the query parameter and the initial value to the hook. In return, we receive identical value and set value pairs as we would with the `useState` hook. The only deviation is that the value is sustained in the URL query string.

## Role of useRouter and useState within useQueryParamState Hook

Our hook will be a generic function, allowing us to support various types of values. However, the query parameters will most commonly be storing either a string or a number.

```tsx
import { useRouter } from "next/router"
import { useCallback, useState } from "react"
import { useHandleQueryParams } from "./useHandleQueryParams"

const parseValue = <T,>(value: any): T => {
  try {
    return JSON.parse(value) as T
  } catch {
    return value as never as T
  }
}

export const useQueryParamState = <T extends number | string>(
  key: string,
  defaultValue: T
) => {
  const [value, setValue] = useState<T>(defaultValue)

  const { push, query, pathname } = useRouter()

  const onChange = useCallback(
    (newValue: T) => {
      push({
        pathname,
        query: {
          ...query,
          [key]: newValue,
        },
      })
    },
    [key, pathname, query, push]
  )

  useHandleQueryParams<Record<string, T | undefined>>((params) => {
    if (params[key] === undefined) {
      return
    }

    const newValue = parseValue<T>(params[key])
    if (newValue === value) return

    setValue(newValue)
  })

  return [value, onChange] as const
}
```

## Avoiding Default Value Update in the Query Param

Within the `useQueryParamState` hook, we continue to rely on the `useState` hook to preserve the value in the component state. This is done to force a re-render when the value changes. We also utilize the `useRouter` hook from NextJS to gain access to the present URL query string and the `push` function. The latter will be used to update the URL query string – you could alternatively use the `replace` function if you prefer not to add a new entry to the browser history with every change in value.

## The Role of onChange Function

In this hook, we avoid updating the query param with the default value. This only transpires when the user alters the value. Depending on your app, you might want to set the default value in the URL query string upon the first render if it isn't already present.

## The Importance of Parsing Value Type

Instead of the `setValue` function, the hook's consumer is provided with the `onChange` function. This ensures that the value is consistently updated in the URL query string, and not just within the component state. We avoid invoking the `setValue` function inside the `onChange` function since we already monitor for query param alterations in the `useHandleQueryParams` hook. Here, we verify whether the query parameter with the designated key exists in the URL query string. If present, we parse the value and accordingly update the component state. Although the `query` object derived from the `useRouter` hook is "parsed", it has been observed that it often returns numbers as strings. Therefore, it's preferable to undertake an additional parsing step to ensure the correct value type.

```tsx
import { useRouter } from "next/router"
import { useEffect } from "react"

type QueryParamsHandler<T> = (params: T) => void

export const useHandleQueryParams = <T,>(handler: QueryParamsHandler<T>) => {
  const { isReady, query } = useRouter()

  useEffect(() => {
    if (!isReady) return

    handler(query as unknown as T)
  }, [isReady, query, handler])
}
```

## Description of useHandleQueryParams Hook

The `useHandleQueryParams` hook is a simple wrapper around the `useEffect` hook. It calls the handler function once the router is ready and the query parameters are accessible. We need to perform a type assertion because the `query` type renders every field as optional. However, we will regularly employ this hook in situations where we are certain that the query parameters are present. A typical scenario would be on an authentication page, where we would normally expect to receive a code from the OAuth provider. Without this query parameter, the process could not be completed.