āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā š shadcn/directory/47ng/nuqs/parsers/making-your-own ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā
import { CustomParserDemo, CustomMultiParserDemo } from '@/content/docs/parsers/demos'
You may wish to customise the rendered query string for your data type.
For this, nuqs exposes the createParser{:ts} function to make your own parsers.
You pass it two functions:
parse{:ts}: a function that takes a string and returns the parsed value, or null{:ts} if invalid.serialize{:ts}: a function that takes the parsed value and returns a string.import { createParser } from 'nuqs'
const parseAsStarRating = createParser({
// [!code word:parse]
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)
},
// [!code word:serialize]
serialize(value) {
return Array.from({length: value}, () => 'ā
').join('')
}
})
<Suspense>
<CustomParserDemo/>
</Suspense>
For state types that can't be compared by the ==={:ts} operator, you'll need to
provide an eq{:ts} 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'}`
},
// [!code highlight:3]
eq(a, b) {
return a.id === b.id && a.desc === b.desc
}
})
This is used for the clearOnDefault{:ts} option,
to check if the current value is equal to the default value.
The parsers we've seen until now are SingleParsers{:ts}: 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{:ts} work similar to SingleParsers{:ts}, except that they operate on arrays, to support key repetition:
import { Querystring } from '@/src/components/querystring'
<Querystring path="/" value="?tag=type-safe&tag=url-state&tag=react" />This means:
parse{:ts} takes an Array<string>{:ts}. It receives all matching values of the key it operates on, and returns the parsed value, or null{:ts} if invalid.serialize{:ts} takes the parsed value and returns an Array<string>{:ts}, where each item will be separately added to the URL.You can then compose & reduce this array to form complex data types:
<Suspense> <CustomMultiParserDemo /> </Suspense>/**
* 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({})
)
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.
ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā