Skip to content

Commit d802c73

Browse files
committed
add maxExecutionTime to retryer
1 parent 3173d1c commit d802c73

File tree

11 files changed

+782
-10
lines changed

11 files changed

+782
-10
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Example
2+
3+
To run this example:
4+
5+
- `npm install`
6+
- `npm run dev`
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/svg+xml" href="/emblem-light.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>TanStack Pacer - useAsyncThrottledCallback Example</title>
8+
</head>
9+
<body>
10+
<div id="root"></div>
11+
<script type="module" src="/src/index.tsx"></script>
12+
</body>
13+
</html>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"name": "@tanstack/pacer-example-react-use-async-throttled-callback",
3+
"private": true,
4+
"type": "module",
5+
"scripts": {
6+
"dev": "vite --port=3005",
7+
"build": "vite build",
8+
"preview": "vite preview",
9+
"test:types": "tsc"
10+
},
11+
"dependencies": {
12+
"@tanstack/react-pacer": "^0.16.4",
13+
"react": "^19.1.1",
14+
"react-dom": "^19.1.1"
15+
},
16+
"devDependencies": {
17+
"@types/react": "^19.1.17",
18+
"@types/react-dom": "^19.1.11",
19+
"@vitejs/plugin-react": "^5.0.4",
20+
"vite": "^7.1.7"
21+
},
22+
"browserslist": {
23+
"production": [
24+
">0.2%",
25+
"not dead",
26+
"not op_mini all"
27+
],
28+
"development": [
29+
"last 1 chrome version",
30+
"last 1 firefox version",
31+
"last 1 safari version"
32+
]
33+
}
34+
}
Lines changed: 13 additions & 0 deletions
Loading
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
import { useState } from 'react'
2+
import ReactDOM from 'react-dom/client'
3+
import { useAsyncThrottledCallback } from '@tanstack/react-pacer/async-throttler'
4+
5+
interface SearchResult {
6+
id: number
7+
title: string
8+
}
9+
10+
// Simulate API call with fake data
11+
const fakeApi = async (term: string): Promise<Array<SearchResult>> => {
12+
await new Promise((resolve) => setTimeout(resolve, 500)) // Simulate network delay
13+
if (term === 'error') {
14+
throw new Error('Simulated API error')
15+
}
16+
return [
17+
{ id: 1, title: `${term} result ${Math.floor(Math.random() * 100)}` },
18+
{ id: 2, title: `${term} result ${Math.floor(Math.random() * 100)}` },
19+
{ id: 3, title: `${term} result ${Math.floor(Math.random() * 100)}` },
20+
]
21+
}
22+
23+
function App1() {
24+
const [searchTerm, setSearchTerm] = useState('')
25+
const [results, setResults] = useState<Array<SearchResult>>([])
26+
const [isLoading, setIsLoading] = useState(false)
27+
const [error, setError] = useState<string | null>(null)
28+
29+
// Create async throttled function - Stable reference provided by useAsyncThrottledCallback
30+
const throttledSearch = useAsyncThrottledCallback(
31+
async (term: string) => {
32+
if (!term.trim()) {
33+
setResults([])
34+
return []
35+
}
36+
37+
setIsLoading(true)
38+
setError(null)
39+
40+
try {
41+
const data = await fakeApi(term)
42+
setResults(data)
43+
return data
44+
} catch (err) {
45+
const errorMessage =
46+
err instanceof Error ? err.message : 'Unknown error'
47+
setError(errorMessage)
48+
setResults([])
49+
throw err
50+
} finally {
51+
setIsLoading(false)
52+
}
53+
},
54+
{
55+
wait: 1000,
56+
// leading: true, // optional, defaults to true
57+
// trailing: true, // optional, defaults to true
58+
},
59+
)
60+
61+
async function handleSearchChange(e: React.ChangeEvent<HTMLInputElement>) {
62+
const newValue = e.target.value
63+
setSearchTerm(newValue)
64+
65+
try {
66+
await throttledSearch(newValue)
67+
} catch (err) {
68+
// Error is already handled in the throttled function
69+
console.log('Search failed:', err)
70+
}
71+
}
72+
73+
return (
74+
<div>
75+
<h1>TanStack Pacer useAsyncThrottledCallback Example 1</h1>
76+
<div>
77+
<input
78+
autoFocus
79+
type="search"
80+
value={searchTerm}
81+
onChange={handleSearchChange}
82+
placeholder="Type to search... (try 'error' to see error handling)"
83+
style={{ width: '100%', marginBottom: '10px' }}
84+
/>
85+
</div>
86+
87+
{isLoading && <p style={{ color: 'blue' }}>Searching...</p>}
88+
{error && <p style={{ color: 'red' }}>Error: {error}</p>}
89+
90+
<div>
91+
<p>Current search term: {searchTerm}</p>
92+
{results.length > 0 && (
93+
<div>
94+
<h3>Results:</h3>
95+
<ul>
96+
{results.map((result) => (
97+
<li key={result.id}>{result.title}</li>
98+
))}
99+
</ul>
100+
</div>
101+
)}
102+
</div>
103+
</div>
104+
)
105+
}
106+
107+
function App2() {
108+
const [count, setCount] = useState(0)
109+
const [apiCallCount, setApiCallCount] = useState(0)
110+
111+
// Simulate API call that returns a value
112+
const incrementApi = async (value: number): Promise<number> => {
113+
await new Promise((resolve) => setTimeout(resolve, 300))
114+
const newCount = value + 1
115+
setApiCallCount((prev) => prev + 1)
116+
return newCount
117+
}
118+
119+
// Create async throttled increment function
120+
const throttledIncrement = useAsyncThrottledCallback(
121+
async (currentValue: number) => {
122+
const result = await incrementApi(currentValue)
123+
setCount(result)
124+
return result
125+
},
126+
{
127+
wait: 1000,
128+
leading: true, // Execute immediately on first call
129+
trailing: true, // Execute after throttle period ends
130+
},
131+
)
132+
133+
function handleIncrement() {
134+
// Update local state immediately for instant feedback
135+
setCount((prev) => {
136+
const newCount = prev + 1
137+
throttledIncrement(newCount)
138+
return newCount
139+
})
140+
}
141+
142+
return (
143+
<div>
144+
<h1>TanStack Pacer useAsyncThrottledCallback Example 2</h1>
145+
<table>
146+
<tbody>
147+
<tr>
148+
<td>Current Count:</td>
149+
<td>{count}</td>
150+
</tr>
151+
<tr>
152+
<td>API Calls Made:</td>
153+
<td>{apiCallCount}</td>
154+
</tr>
155+
</tbody>
156+
</table>
157+
<div>
158+
<button onClick={handleIncrement}>
159+
Increment (throttled API call)
160+
</button>
161+
</div>
162+
<p style={{ fontSize: '0.9em', color: '#666' }}>
163+
Click rapidly - API calls are throttled to 1 second, but UI updates
164+
immediately. First click executes immediately, then at most once per
165+
second.
166+
</p>
167+
</div>
168+
)
169+
}
170+
171+
function App3() {
172+
const [scrollPosition, setScrollPosition] = useState(0)
173+
const [saveCount, setSaveCount] = useState(0)
174+
const [lastSaved, setLastSaved] = useState<Date | null>(null)
175+
const [isSaving, setIsSaving] = useState(false)
176+
177+
// Simulate saving scroll position to server
178+
const saveScrollPosition = async (
179+
position: number,
180+
): Promise<{ success: boolean; position: number }> => {
181+
await new Promise((resolve) => setTimeout(resolve, 300))
182+
return { success: true, position }
183+
}
184+
185+
// Create throttled save function
186+
const throttledSave = useAsyncThrottledCallback(
187+
async (position: number) => {
188+
setIsSaving(true)
189+
190+
try {
191+
const result = await saveScrollPosition(position)
192+
setSaveCount((prev) => prev + 1)
193+
setLastSaved(new Date())
194+
return result
195+
} finally {
196+
setIsSaving(false)
197+
}
198+
},
199+
{
200+
wait: 1000,
201+
leading: true,
202+
trailing: true,
203+
},
204+
)
205+
206+
function handleScroll(e: React.UIEvent<HTMLDivElement>) {
207+
const position = e.currentTarget.scrollTop
208+
setScrollPosition(position)
209+
throttledSave(position)
210+
}
211+
212+
return (
213+
<div>
214+
<h1>TanStack Pacer useAsyncThrottledCallback Example 3</h1>
215+
<div
216+
style={{
217+
height: '200px',
218+
overflow: 'auto',
219+
border: '1px solid #ccc',
220+
padding: '10px',
221+
marginBottom: '20px',
222+
}}
223+
onScroll={handleScroll}
224+
>
225+
<div style={{ height: '1000px' }}>
226+
<p>Scroll this area to trigger throttled saves!</p>
227+
<p>Current scroll position: {Math.round(scrollPosition)}px</p>
228+
{isSaving && <p style={{ color: 'blue' }}>Saving position...</p>}
229+
<div style={{ marginTop: '20px' }}>
230+
<p>Saves triggered: {saveCount}</p>
231+
{lastSaved && (
232+
<p>Last saved at: {lastSaved.toLocaleTimeString()}</p>
233+
)}
234+
</div>
235+
<div style={{ marginTop: '40px' }}>
236+
<p>Keep scrolling...</p>
237+
<p style={{ marginTop: '100px' }}>More content...</p>
238+
<p style={{ marginTop: '100px' }}>Even more content...</p>
239+
<p style={{ marginTop: '100px' }}>Almost there...</p>
240+
<p style={{ marginTop: '100px' }}>You made it to the end!</p>
241+
</div>
242+
</div>
243+
</div>
244+
245+
<p style={{ fontSize: '0.9em', color: '#666' }}>
246+
Scroll position is saved at most once per second, but updates instantly
247+
on screen
248+
</p>
249+
</div>
250+
)
251+
}
252+
253+
const root = ReactDOM.createRoot(document.getElementById('root')!)
254+
root.render(
255+
<div>
256+
<App1 />
257+
<hr />
258+
<App2 />
259+
<hr />
260+
<App3 />
261+
</div>,
262+
)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ESNext",
4+
"lib": ["DOM", "DOM.Iterable", "ESNext"],
5+
"module": "ESNext",
6+
"skipLibCheck": true,
7+
8+
/* Bundler mode */
9+
"moduleResolution": "Bundler",
10+
"allowImportingTsExtensions": true,
11+
"resolveJsonModule": true,
12+
"isolatedModules": true,
13+
"noEmit": true,
14+
"jsx": "react-jsx",
15+
16+
/* Linting */
17+
"strict": true,
18+
"noUnusedLocals": true,
19+
"noUnusedParameters": true,
20+
"noFallthroughCasesInSwitch": true
21+
},
22+
"include": ["src", "vite.config.ts"]
23+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { defineConfig } from 'vite'
2+
import react from '@vitejs/plugin-react'
3+
4+
// https://vitejs.dev/config/
5+
export default defineConfig({
6+
plugins: [react()],
7+
})

0 commit comments

Comments
 (0)