Custom parsers
Making your own parsers for custom data types & pretty URLs
You may wish to customise the rendered query string for your data type.
For this, nuqs exposes the createParser function to make your own parsers.
You pass it two functions:
parse: a function that takes a string and returns the parsed value, ornullif invalid.serialize: a function that takes the parsed value and returns a string.
import { createParser } from 'nuqs'
const parseAsStarRating = createParser({
parse(queryValue) {
const inBetween = queryValue.split('★')
const isValid = inBetween.length > 1 && inBetween.every(s => s === '')
if (!isValid) return null
const numStars = inBetween.length - 1
return Math.min(5, numStars)
},
serialize(value) {
return Array.from({length: value}, () => '★').join('')
}
})Equality function
For state types that can’t be compared by the === operator, you’ll need to
provide an eq function as well:
// Eg: TanStack Table sorting state
// /?sort=foo:asc → { id: 'foo', desc: false }
const parseAsSort = createParser({
parse(query) {
const [key = '', direction = ''] = query.split(':')
const desc = parseAsStringLiteral(['asc', 'desc']).parse(direction) ?? 'asc'
return {
id: key,
desc: desc === 'desc'
}
},
serialize(value) {
return `${value.id}:${value.desc ? 'desc' : 'asc'}`
},
eq(a, b) {
return a.id === b.id && a.desc === b.desc
}
})This is used for the clearOnDefault option,
to check if the current value is equal to the default value.
Custom Multi Parsers
The parsers we’ve seen until now are SingleParsers: they operate on the first occurence of the
key in the URL, and give you a string value to parse when it’s available.
MultiParsers work similar to SingleParsers, except that they operate on arrays, to support key repetition:
/?tag=type-safe&tag=url-state &tag=react
This means:
parsetakes anArray<string>. It receives all matching values of the key it operates on, and returns the parsed value, ornullif invalid.serializetakes the parsed value and returns anArray<string>, where each item will be separately added to the URL.
You can then compose & reduce this array to form complex data types:
/**
* 100~200 <=> { gte: 100, lte: 200 }
* 150 <=> { eq: 150 }
*/
const parseAsFromTo = createParser({
parse: value => {
const [min = null, max = null] = value.split('~').map(parseAsInteger.parse)
if (min === null) return null
if (max === null) return { eq: min }
return { gte: min, lte: max }
},
serialize: value => {
return value.eq !== undefined ? String(value.eq) : `${value.gte}~${value.lte}`
}
})
/**
* foo:bar <=> { key: 'foo', value: 'bar' }
*/
const parseAsKeyValue = createParser({
parse: value => {
const [key, val] = value.split(':')
if (!key || !val) return null
return { key, value: val }
},
serialize: value => {
return `${value.key}:${value.value}`
}
})
const parseAsFilters = <TItem extends {}>(itemParser: SingleParser<TItem>) => {
return createMultiParser({
parse: values => {
const keyValue = values.map(parseAsKeyValue.parse).filter(v => v !== null)
const result = Object.fromEntries(
keyValue.flatMap(({ key, value }) => {
const parsedValue: TItem | null = itemParser.parse(value)
return parsedValue === null ? [] : [[key, parsedValue]]
})
)
return Object.keys(result).length === 0 ? null : result
},
serialize: values => {
return Object.entries(values).map(([key, value]) => {
if (!itemParser.serialize) return null
return parseAsKeyValue.serialize({ key, value: itemParser.serialize(value) })
}).filter(v => v !== null)
}
})
}
const [filters, setFilters] = useQueryState(
'filters',
parseAsFilters(parseAsFromTo).withDefault({})
)Caveat: lossy serializers
If your serializer loses precision or doesn’t accurately represent the underlying state value, you will lose this precision when reloading the page or restoring state from the URL (eg: on navigation).
Example:
const geoCoordParser = {
parse: parseFloat,
serialize: v => v.toFixed(4) // Loses precision
}
const [lat, setLat] = useQueryState('lat', geoCoordParser)Here, setting a latitude of 1.23456789 will render a URL query string
of lat=1.2345, while the internal lat state will be correctly
set to 1.23456789.
Upon reloading the page, the state will be incorrectly set to 1.2345.