Skip to content

Commit 20ce7b9

Browse files
authored
fix: Increase rate-limit factor for Next.js app router (#973)
1 parent c65a113 commit 20ce7b9

File tree

12 files changed

+246
-13
lines changed

12 files changed

+246
-13
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { RateLimits } from 'e2e-shared/specs/rate-limits'
2+
import { Suspense } from 'react'
3+
4+
export default function Page() {
5+
return (
6+
<Suspense>
7+
<RateLimits />
8+
</Suspense>
9+
)
10+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { RateLimits } from 'e2e-shared/specs/rate-limits'
2+
3+
export default RateLimits

packages/e2e/react-router/v6/src/react-router.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ const router = createBrowserRouter(
4949
<Route path="pretty-urls" lazy={load(import('./routes/pretty-urls'))} />
5050
<Route path="dynamic-segments/dynamic/:segment" lazy={load(import('./routes/dynamic-segments.dynamic.$segment'))} />
5151
<Route path="dynamic-segments/catch-all?*" lazy={load(import('./routes/dynamic-segments.catch-all.$'))} />
52+
<Route path="rate-limits" lazy={load(import('./routes/rate-limits'))} />
5253

5354
<Route path="render-count/:hook/:shallow/:history/:startTransition/no-loader" lazy={load(import('./routes/render-count.$hook.$shallow.$history.$startTransition.no-loader'))} />
5455
<Route path="render-count/:hook/:shallow/:history/:startTransition/sync-loader" lazy={load(import('./routes/render-count.$hook.$shallow.$history.$startTransition.sync-loader'))} />
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { RateLimits } from 'e2e-shared/specs/rate-limits'
2+
3+
export default RateLimits

packages/e2e/react-router/v7/app/routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export default [
3232
route('/pretty-urls', './routes/pretty-urls.tsx'),
3333
route('/dynamic-segments/dynamic/:segment', './routes/dynamic-segments.dynamic.$segment.tsx'),
3434
route('/dynamic-segments/catch-all?/*', './routes/dynamic-segments.catch-all.$.tsx'),
35+
route('/rate-limits', './routes/rate-limits.tsx'),
3536

3637
route('/render-count/:hook/:shallow/:history/:startTransition/no-loader', './routes/render-count.$hook.$shallow.$history.$startTransition.no-loader.tsx'),
3738
route('/render-count/:hook/:shallow/:history/:startTransition/sync-loader', './routes/render-count.$hook.$shallow.$history.$startTransition.sync-loader.tsx'),
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { RateLimits } from 'e2e-shared/specs/rate-limits'
2+
3+
export default RateLimits

packages/e2e/react/src/main.tsx

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
11
import { NuqsAdapter, enableHistorySync } from 'nuqs/adapters/react'
2-
import { StrictMode } from 'react'
32
import { createRoot } from 'react-dom/client'
43
import { RootLayout } from './layout'
54
import { Router } from './routes'
65

76
enableHistorySync()
87

98
createRoot(document.getElementById('root')!).render(
10-
<StrictMode>
11-
<NuqsAdapter
12-
fullPageNavigationOnShallowFalseUpdates={
13-
process.env.FULL_PAGE_NAV_ON_SHALLOW_FALSE === 'true'
14-
}
15-
>
16-
<RootLayout>
17-
<Router />
18-
</RootLayout>
19-
</NuqsAdapter>
20-
</StrictMode>
9+
// <StrictMode>
10+
<NuqsAdapter
11+
fullPageNavigationOnShallowFalseUpdates={
12+
process.env.FULL_PAGE_NAV_ON_SHALLOW_FALSE === 'true'
13+
}
14+
>
15+
<RootLayout>
16+
<Router />
17+
</RootLayout>
18+
</NuqsAdapter>
19+
// </StrictMode>
2120
)

packages/e2e/react/src/routes.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const routes: Record<string, React.LazyExoticComponent<() => JSX.Element>> = {
2525
'/conditional-rendering/useQueryStates': lazy(() => import('./routes/conditional-rendering.useQueryStates')),
2626
'/scroll': lazy(() => import('./routes/scroll')),
2727
'/pretty-urls': lazy(() => import('./routes/pretty-urls')),
28+
'/rate-limits': lazy(() => import('./routes/rate-limits')),
2829

2930
'/render-count/useQueryState/true/replace/false': lazy(() => import('./routes/render-count')),
3031
'/render-count/useQueryState/true/replace/true': lazy(() => import('./routes/render-count')),
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { RateLimits } from 'e2e-shared/specs/rate-limits'
2+
3+
export default RateLimits
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { RateLimits } from 'e2e-shared/specs/rate-limits'
2+
3+
export default RateLimits
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
'use client'
2+
3+
import { parseAsInteger, useQueryState } from 'nuqs'
4+
import { useCallback, useReducer } from 'react'
5+
6+
const UPDATE_RATE_MS = 20
7+
const RUN_TIME_MS = 60_000
8+
9+
type UpdateSuccess = {
10+
outcome: 'success'
11+
time: number
12+
searchParams: URLSearchParams
13+
}
14+
type UpdateFailure = {
15+
outcome: 'failure'
16+
time: number
17+
error: unknown
18+
}
19+
20+
type UpdateOutcome = UpdateSuccess | UpdateFailure
21+
22+
type TestHookResult = {
23+
start: () => void
24+
stop: () => void
25+
reset: () => void
26+
startedAt: number | null
27+
currentCount: number
28+
successfulUpdates: UpdateSuccess[]
29+
failedUpdates: UpdateFailure[]
30+
}
31+
32+
type ReducerState = {
33+
successfulUpdates: UpdateSuccess[]
34+
failedUpdates: UpdateFailure[]
35+
controller: AbortController | null
36+
startedAt: number | null
37+
}
38+
39+
type ReducerAction =
40+
| { type: 'start'; payload: AbortController }
41+
| { type: 'stop' }
42+
| { type: 'reset' }
43+
| { type: 'update'; payload: UpdateOutcome }
44+
45+
function reducer(state: ReducerState, action: ReducerAction): ReducerState {
46+
performance.mark(`[nuqs] dispatch ${JSON.stringify(action)}`)
47+
switch (action.type) {
48+
case 'start':
49+
state.controller?.abort()
50+
return {
51+
...state,
52+
startedAt: performance.now(),
53+
controller: action.payload
54+
}
55+
case 'stop':
56+
state.controller?.abort()
57+
return {
58+
...state,
59+
controller: null,
60+
startedAt: null
61+
}
62+
case 'reset':
63+
return {
64+
...state,
65+
successfulUpdates: [],
66+
failedUpdates: []
67+
}
68+
case 'update':
69+
const key =
70+
action.payload.outcome === 'success'
71+
? 'successfulUpdates'
72+
: 'failedUpdates'
73+
if (
74+
state[key].findIndex(
75+
(u: UpdateOutcome) => u.time === action.payload.time
76+
) !== -1
77+
) {
78+
return state
79+
}
80+
return {
81+
...state,
82+
[key]: [...state[key], action.payload]
83+
}
84+
default:
85+
throw new Error(`Unknown action type for ${action}`)
86+
}
87+
}
88+
89+
const initialState: ReducerState = {
90+
successfulUpdates: [],
91+
failedUpdates: [],
92+
controller: null,
93+
startedAt: null
94+
}
95+
96+
function useRateLimitTest(): TestHookResult {
97+
const [currentCount, setCount] = useQueryState(
98+
'count',
99+
parseAsInteger.withDefault(0)
100+
)
101+
const [state, dispatch] = useReducer(reducer, initialState)
102+
103+
const start = useCallback(() => {
104+
const controller = new AbortController()
105+
dispatch({
106+
type: 'start',
107+
payload: controller
108+
})
109+
const signal = controller.signal
110+
const timeout = setTimeout(() => dispatch({ type: 'stop' }), RUN_TIME_MS)
111+
let i = 1
112+
const interval = setInterval(() => {
113+
try {
114+
setCount(i++)
115+
.then(searchParams => {
116+
dispatch({
117+
type: 'update',
118+
payload: {
119+
outcome: 'success',
120+
time: performance.now(),
121+
searchParams
122+
}
123+
})
124+
})
125+
.catch(error => {
126+
dispatch({
127+
type: 'update',
128+
payload: {
129+
outcome: 'failure',
130+
time: performance.now(),
131+
error
132+
}
133+
})
134+
})
135+
} catch (error) {
136+
dispatch({
137+
type: 'update',
138+
payload: {
139+
outcome: 'failure',
140+
time: performance.now(),
141+
error
142+
}
143+
})
144+
}
145+
}, UPDATE_RATE_MS)
146+
signal.addEventListener('abort', () => {
147+
clearTimeout(timeout)
148+
clearInterval(interval)
149+
})
150+
}, [])
151+
const stop = useCallback(() => dispatch({ type: 'stop' }), [])
152+
const reset = useCallback(() => {
153+
setCount(null)
154+
dispatch({ type: 'reset' })
155+
}, [])
156+
157+
return {
158+
start,
159+
stop,
160+
reset,
161+
currentCount,
162+
startedAt: state.startedAt,
163+
failedUpdates: state.failedUpdates,
164+
successfulUpdates: state.successfulUpdates
165+
}
166+
}
167+
168+
export function RateLimits() {
169+
const {
170+
currentCount,
171+
startedAt,
172+
successfulUpdates,
173+
failedUpdates,
174+
reset,
175+
start,
176+
stop
177+
} = useRateLimitTest()
178+
179+
return (
180+
<>
181+
<button onClick={start} disabled={startedAt !== null}>
182+
Start
183+
</button>
184+
<button onClick={stop} disabled={startedAt === null}>
185+
Stop
186+
</button>
187+
<button onClick={reset} disabled={startedAt !== null}>
188+
Reset
189+
</button>
190+
<p>Count: {currentCount}</p>
191+
<p>Total: {successfulUpdates.length + failedUpdates.length}</p>
192+
<p>Success: {successfulUpdates.length}</p>
193+
<p>Errors: {failedUpdates.length}</p>
194+
{failedUpdates.length > 0 && (
195+
<ul>
196+
{failedUpdates.map(({ time, error }, i) => (
197+
<li key={i}>
198+
+{time - (startedAt ?? 0)}: {String(error)}
199+
</li>
200+
))}
201+
</ul>
202+
)}
203+
</>
204+
)
205+
}

packages/nuqs/src/adapters/next/impl.app.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ export function useNuqsNextAppRouterAdapter(): AdapterInterface {
4545
searchParams: optimisticSearchParams,
4646
updateUrl,
4747
// See: https://github.com/47ng/nuqs/issues/603#issuecomment-2317057128
48-
rateLimitFactor: 2
48+
// and https://github.com/47ng/nuqs/discussions/960#discussioncomment-12699171
49+
rateLimitFactor: 3
4950
}
5051
}
5152

0 commit comments

Comments
 (0)