@@ -130,43 +130,145 @@ useQueryState('foo', { scroll: true })
130130```
131131
132132
133- ## Throttling URL updates
133+ ## Rate-limiting URL updates
134134
135135Because of browsers rate-limiting the History API, updates ** to the
136136URL** are queued and throttled to a default of 50ms, which seems to satisfy
137137most browsers even when sending high-frequency query updates, like binding
138138to a text input or a slider.
139139
140- Safari's rate limits are much higher and require a throttle of 120ms (320ms for older
141- versions of Safari).
140+ Safari's rate limits are much higher and use a default throttle of 120ms
141+ (320ms for older versions of Safari).
142142
143- If you want to opt-in to a larger throttle time -- for example to reduce the amount
143+ <Callout title = " Note" >
144+ the state returned by the hook is always updated ** instantly** , to keep UI responsive.
145+ Only changes to the URL, and server requests when using ` shallow: false{:ts} ` , are throttled.
146+ </Callout >
147+
148+ This throttle time is configurable, and also allows you to debounce updates
149+ instead.
150+
151+ <Callout title = " Which one should I use?" >
152+ Throttle will emit the first update immediately, then batch updates at a slower
153+ pace ** regularly** . This is recommended for most low-frequency updates.
154+
155+ Debounce will push back the moment when the URL is updated when you set your state,
156+ making it ** eventually consistent** . This is recommended for high-frequency
157+ updates where the last value is more interesting than the intermediate ones,
158+ like a search input or moving a slider.
159+
160+ Read more about [ debounce vs throttle] ( https://kettanaito.com/blog/debounce-vs-throttle ) .
161+ </Callout >
162+
163+ ### Throttle
164+
165+ If you want to increase the throttle time -- for example to reduce the amount
144166of requests sent to the server when paired with ` shallow: false{:ts} ` -- you can
145- specify it under the ` throttleMs ` option:
167+ specify it under the ` limitUrlUpdates ` option:
146168
147- ``` tsx
148- // [!code word:throttleMs]
169+ ``` ts /limitUrlUpdates/
149170useQueryState (' foo' , {
150171 // Send updates to the server maximum once every second
151172 shallow: false ,
152- throttleMs: 1000
173+ limitUrlUpdates: {
174+ method: ' throttle' ,
175+ timeMs: 1000
176+ }
153177})
154- ```
155178
156- <Callout title = " Note" >
157- the state returned by the hook is always updated ** instantly** , to keep UI responsive.
158- Only changes to the URL, and server requests when using ` shallow: false{:ts} ` , are throttled.
159- </Callout >
179+ // or using the shorthand:
180+ import { throttle } from ' nuqs'
181+
182+ useQueryState (' foo' , {
183+ shallow: false ,
184+ limitUrlUpdates: throttle (1000 )
185+ })
186+ ```
160187
161188If multiple hooks set different throttle values on the same event loop tick,
162189the highest value will be used. Also, values lower than 50ms will be ignored,
163190to avoid rate-limiting issues.
164191[ Read more] ( https://francoisbest.com/posts/2023/storing-react-state-in-the-url-with-nextjs#batching--throttling ) .
165192
166- Specifying a ` +Infinity{:ts} ` value for ` throttleMs{:ts} ` will ** disable** updates to the
193+ Specifying a ` +Infinity{:ts} ` value for throttle time will ** disable** updates to the
167194URL or the server, but all ` useQueryState(s) ` hooks will still update their
168195internal state and stay in sync with each other.
169196
197+ <Callout title = " Deprecation notice" >
198+ The
` throttleMs ` option has been deprecated in
` [email protected] ` and will be removed
199+ in a later major upgrade.
200+
201+ To migrate:
202+ 1 . ` import { throttle } from 'nuqs' {:ts} `
203+ 2 . Replace ` { throttleMs: 100 }{:ts} ` with ` { limitUrlUpdates: throttle(100) }{:ts} ` in your options.
204+ </Callout >
205+
206+ ### Debounce
207+
208+ In addition to throttling, you can apply a debouncing mechanism to state updates,
209+ to delay the moment where the URL gets updated with the latest value.
210+
211+ This can be useful for high frequency state updates where you only care about
212+ the final value, not all the intermediary ones while typing in a search input
213+ or moving a slider.
214+
215+ We recommend you opt-in to debouncing on specific state updates, rather than
216+ defining it for the whole search param.
217+
218+ Let's take the example of a search input. You'll want to update it:
219+
220+ 1 . When the user is typing text, with debouncing
221+ 2 . When the user clears the input, by sending an immediate update
222+ 3 . When the user presses Enter, by sending an immediate update
223+
224+ You can see the debounce case is the outlier here, and actually conditioned on
225+ the set value, so we can specify it using the state updater function:
226+
227+ ``` tsx
228+ import { useQueryState , parseAsString , debounce } from ' nuqs' ;
229+
230+ function Search() {
231+ const [search, setSearch] = useQueryState (
232+ ' q' ,
233+ parseAsString .withDefault (' ' ).withOptions ({ shallow: false })
234+ )
235+
236+ return (
237+ <input
238+ value = { search }
239+ onChange = { (e ) =>
240+ setSearch (e .target .value , {
241+ // Send immediate update if resetting, otherwise debounce at 500ms
242+ limitUrlUpdates: e .target .value === ' ' ? undefined : debounce (500 )
243+ })
244+ }
245+ onKeyPress = { (e ) => {
246+ if (e .key === ' Enter' ) {
247+ // Send immediate update
248+ setSearch (e .target .value )
249+ }
250+ }}
251+ />
252+ )
253+ }
254+ ```
255+
256+ ### Resetting
257+
258+ You can use the ` defaultRateLimit{:ts} ` import to reset debouncing or throttling to
259+ the default:
260+
261+ ``` ts /defaultRateLimit/
262+ import { debounce , defaultRateLimit } from ' nuqs'
263+
264+ const [, setState] = useQueryState (' foo' , {
265+ limitUrlUpdates: debounce (1000 )
266+ })
267+
268+ // This state update isn't debounced
269+ setState (' bar' , { limitUrlUpdates: defaultRateLimit })
270+ ```
271+
170272
171273## Transitions
172274
0 commit comments