useQueryState hook for Next.js - Like React.useState, but stored in the URL query string
- 🧘♀️ Simple: the URL is the source of truth.
- 🕰 Replace history or append to use the Back button to navigate state updates
- ⚡️ Built-in converters for common object types (number, float, boolean, Date)
- ♊️ Linked querystrings with
useQueryStates
- 🔀 Supports both the app router (in client components only) and pages router
$ yarn add next-usequerystate
or
$ npm install next-usequerystate
Note: all code samples assume you're using the pages router.
Jump to the app router documentation.
import { useQueryState } from 'next-usequerystate'
export default () => {
const [name, setName] = useQueryState('name')
return (
<>
<h1>Hello, {name || 'anonymous visitor'}!</h1>
<input value={name || ''} onChange={e => setName(e.target.value)} />
<button onClick={() => setName(null)}>Clear</button>
</>
)
}
useQueryState
takes one required argument: the key to use in the query string.
Like React.useState
, it returns an array with the value present in the query
string as a string (or null
if none was found), and a state updater function.
Example outputs for our hello world example:
URL | name value | Notes |
---|---|---|
/ |
null |
No name key in URL |
/?name= |
'' |
Empty string |
/?name=foo |
'foo' |
|
/?name=2 |
'2' |
Always returns a string by default, see Parsing below |
If your state type is not a string, you must pass a parsing function in the second argument object.
We provide helpers for common and more advanced object types:
import { queryTypes } from 'next-usequerystate'
useQueryState('tag') // defaults to string
useQueryState('count', queryTypes.integer)
useQueryState('brightness', queryTypes.float)
useQueryState('darkMode', queryTypes.boolean)
useQueryState('after', queryTypes.timestamp) // state is a Date
useQueryState('date', queryTypes.isoDateTime) // state is a Date
useQueryState('array', queryTypes.array(queryTypes.integer)) // state is number[]
useQueryState('json', queryTypes.json<Point>()) // state is a Point
// Enums (string-based only)
enum Direction {
up = 'UP',
down = 'DOWN',
left = 'LEFT',
right = 'RIGHT'
}
const [direction, setDirection] = useQueryState(
'direction',
queryTypes
.stringEnum<Direction>(Object.values(Direction)) // pass a list of allowed values
.withDefault(Direction.up)
)
You may pass a custom set of parse
and serialize
functions:
import { useQueryState } from 'next-usequerystate'
export default () => {
const [hex, setHex] = useQueryState('hex', {
// TypeScript will automatically infer it's a number
// based on what `parse` returns.
parse: (query: string) => parseInt(query, 16),
serialize: value => value.toString(16)
})
}
Example: simple counter stored in the URL:
import { useQueryState, queryTypes } from 'next-usequerystate'
export default () => {
const [count, setCount] = useQueryState('count', queryTypes.integer)
return (
<>
<pre>count: {count}</pre>
<button onClick={() => setCount(0)}>Reset</button>
<button onClick={() => setCount(c => c ?? 0 + 1)}>+</button>
<button onClick={() => setCount(c => c ?? 0 - 1)}>-</button>
<button onClick={() => setCount(null)}>Clear</button>
</>
)
}
When the query string is not present in the URL, the default behaviour is to
return null
as state.
As you saw in the previous example, it makes state updating and UI rendering tedious.
You can specify a default value to be returned in this case:
const [count, setCount] = useQueryState(
'count',
queryTypes.integer.withDefault(0)
)
const increment = () => setCount(c => c + 1) // c will never be null
const decrement = () => setCount(c => c - 1) // c will never be null
const clearCount = () => setCount(null) // Remove query from the URL
Note: the default value is internal to React, it will not be written to the URL.
Setting the state to null
will remove the key in the query string and set the
state to the default value.
By default, state updates are done by replacing the current history entry with the updated query when state changes.
You can see this as a sort of git squash
, where all state-changing
operations are merged into a single history value.
You can also opt-in to push a new history item for each state change, per key, which will let you use the Back button to navigate state updates:
// Default: replace current history with new state
useQueryState('foo', { history: 'replace' })
// Append state changes to history:
useQueryState('foo', { history: 'push' })
Any other value for the history
option will fallback to the default.
Note: If using the app router, you don't need to await the state updates.
Because the Next.js router has asynchronous methods, if you want to do multiple
query updates in one go, you'll have to await
them, otherwise the latter will
overwrite the updates of the former:
const MultipleQueriesDemo = () => {
const [lat, setLat] = useQueryState('lat', queryTypes.float)
const [lng, setLng] = useQueryState('lng', queryTypes.float)
const randomCoordinates = React.useCallback(async () => {
await setLat(Math.random() * 180 - 90)
await setLng(Math.random() * 360 - 180)
}, [])
}
For query keys that should always move together, you can use useQueryStates
with an object containing each key's type:
import { useQueryStates, queryTypes } from 'next-usequerystate'
const [coordinates, setCoordinates] = useQueryStates(
{
lat: queryTypes.float.withDefault(45.18),
lng: queryTypes.float.withDefault(5.72)
},
{
history: 'push'
}
)
const { lat, lng } = coordinates
// Set all (or a subset of) the keys in one go:
await setCoordinates({
lat: Math.random() * 180 - 90,
lng: Math.random() * 360 - 180
})
Note: this feature is only available for the pages router.
By default, Next.js will scroll to the top of the page when changing things in the URL.
To prevent this, router.push()
and router.replace()
have a third optional
parameter to control transitions, which can be passed on the state setter here:
const [name, setName] = useQueryState('name')
setName('Foo', {
scroll: false,
shallow: true // Don't run getStaticProps / getServerSideProps / getInitialProps
})
This hook can be used with the app router in Next.js 13+, but only in client components.
Next.js doesn't allow obtaining querystring parameters from server components.
The API is the same for both hooks, but you'll need to change your imports to:
import {
useQueryState,
useQueryStates,
queryTypes
} from 'next-usequerystate/app' // <- note the /app here
In an later major version, the default import will stop pointing to the pages router implementation and switch to the app router (probably when Next.js starts marking the pages router as deprecated).
In order to lock your usage of the hook to the pages router, you can change your imports to the following:
import { useQueryState } from 'next-usequerystate/pages'
Because the Next.js router is not available in an SSR context, this
hook will always return null
(or the default value if supplied) on SSR/SSG.
- Made with ❤️ by François Best
Using this package at work ? Sponsor me to help with support and maintenance.