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 RadzionKit repository on GitHub.
Here's how to utilize the hook in an app:
const [ticket, setTicket] = useQueryParamState<number>("ticket", 1)
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.
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.
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
}
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.
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.
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.
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])
}
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.