1
- import { useCallback , useEffect , useRef , useState } from "react" ;
1
+ import { useCallback , useEffect , useReducer , useRef } from "react" ;
2
2
import { useEffectEvent } from "./hookPolyfills" ;
3
3
4
4
interface UseRetryOptions {
@@ -55,18 +55,89 @@ interface UseRetryReturn {
55
55
stopRetrying : ( ) => void ;
56
56
}
57
57
58
+ interface RetryState {
59
+ isRetrying : boolean ;
60
+ currentDelay : number | null ;
61
+ attemptCount : number ;
62
+ timeUntilNextRetry : number | null ;
63
+ isManualRetry : boolean ;
64
+ }
65
+
66
+ type RetryAction =
67
+ | { type : "START_RETRY" }
68
+ | { type : "RETRY_SUCCESS" }
69
+ | { type : "RETRY_FAILURE" }
70
+ | { type : "SCHEDULE_RETRY" ; delay : number }
71
+ | { type : "UPDATE_COUNTDOWN" ; timeRemaining : number }
72
+ | { type : "CANCEL_RETRY" }
73
+ | { type : "RESET" }
74
+ | { type : "SET_MANUAL_RETRY" ; isManual : boolean } ;
75
+
76
+ const initialState : RetryState = {
77
+ isRetrying : false ,
78
+ currentDelay : null ,
79
+ attemptCount : 0 ,
80
+ timeUntilNextRetry : null ,
81
+ isManualRetry : false ,
82
+ } ;
83
+
84
+ function retryReducer ( state : RetryState , action : RetryAction ) : RetryState {
85
+ switch ( action . type ) {
86
+ case "START_RETRY" :
87
+ return {
88
+ ...state ,
89
+ isRetrying : true ,
90
+ currentDelay : null ,
91
+ timeUntilNextRetry : null ,
92
+ attemptCount : state . attemptCount + 1 ,
93
+ } ;
94
+ case "RETRY_SUCCESS" :
95
+ return {
96
+ ...initialState ,
97
+ } ;
98
+ case "RETRY_FAILURE" :
99
+ return {
100
+ ...state ,
101
+ isRetrying : false ,
102
+ isManualRetry : false ,
103
+ } ;
104
+ case "SCHEDULE_RETRY" :
105
+ return {
106
+ ...state ,
107
+ currentDelay : action . delay ,
108
+ timeUntilNextRetry : action . delay ,
109
+ } ;
110
+ case "UPDATE_COUNTDOWN" :
111
+ return {
112
+ ...state ,
113
+ timeUntilNextRetry : action . timeRemaining ,
114
+ } ;
115
+ case "CANCEL_RETRY" :
116
+ return {
117
+ ...state ,
118
+ currentDelay : null ,
119
+ timeUntilNextRetry : null ,
120
+ } ;
121
+ case "RESET" :
122
+ return {
123
+ ...initialState ,
124
+ } ;
125
+ case "SET_MANUAL_RETRY" :
126
+ return {
127
+ ...state ,
128
+ isManualRetry : action . isManual ,
129
+ } ;
130
+ default :
131
+ return state ;
132
+ }
133
+ }
134
+
58
135
/**
59
136
* Hook for handling exponential backoff retry logic
60
137
*/
61
138
export function useRetry ( options : UseRetryOptions ) : UseRetryReturn {
62
139
const { onRetry, maxAttempts, initialDelay, maxDelay, multiplier } = options ;
63
- const [ isRetrying , setIsRetrying ] = useState ( false ) ;
64
- const [ currentDelay , setCurrentDelay ] = useState < number | null > ( null ) ;
65
- const [ attemptCount , setAttemptCount ] = useState ( 0 ) ;
66
- const [ timeUntilNextRetry , setTimeUntilNextRetry ] = useState < number | null > (
67
- null ,
68
- ) ;
69
- const [ isManualRetry , setIsManualRetry ] = useState ( false ) ;
140
+ const [ state , dispatch ] = useReducer ( retryReducer , initialState ) ;
70
141
71
142
const timeoutRef = useRef < number | null > ( null ) ;
72
143
const countdownRef = useRef < number | null > ( null ) ;
@@ -95,23 +166,16 @@ export function useRetry(options: UseRetryOptions): UseRetryReturn {
95
166
) ;
96
167
97
168
const performRetry = useCallback ( async ( ) => {
98
- setIsRetrying ( true ) ;
99
- setTimeUntilNextRetry ( null ) ;
100
- setCurrentDelay ( null ) ;
169
+ dispatch ( { type : "START_RETRY" } ) ;
101
170
clearTimers ( ) ;
102
- // Increment attempt count when starting the retry
103
- setAttemptCount ( ( prev ) => prev + 1 ) ;
104
171
105
172
try {
106
173
await onRetryEvent ( ) ;
107
174
// If retry succeeds, reset everything
108
- setAttemptCount ( 0 ) ;
109
- setIsRetrying ( false ) ;
110
- setIsManualRetry ( false ) ;
175
+ dispatch ( { type : "RETRY_SUCCESS" } ) ;
111
176
} catch ( error ) {
112
- // If retry fails, just update state (attemptCount already incremented)
113
- setIsRetrying ( false ) ;
114
- setIsManualRetry ( false ) ;
177
+ // If retry fails, just update state
178
+ dispatch ( { type : "RETRY_FAILURE" } ) ;
115
179
}
116
180
} , [ onRetryEvent , clearTimers ] ) ;
117
181
@@ -123,16 +187,15 @@ export function useRetry(options: UseRetryOptions): UseRetryReturn {
123
187
124
188
// Calculate delay based on attempt - 1 (so first retry gets initialDelay)
125
189
const delay = calculateDelay ( Math . max ( 0 , attempt - 1 ) ) ;
126
- setCurrentDelay ( delay ) ;
127
- setTimeUntilNextRetry ( delay ) ;
190
+ dispatch ( { type : "SCHEDULE_RETRY" , delay } ) ;
128
191
startTimeRef . current = Date . now ( ) ;
129
192
130
193
// Start countdown timer
131
194
countdownRef . current = window . setInterval ( ( ) => {
132
195
if ( startTimeRef . current ) {
133
196
const elapsed = Date . now ( ) - startTimeRef . current ;
134
197
const remaining = Math . max ( 0 , delay - elapsed ) ;
135
- setTimeUntilNextRetry ( remaining ) ;
198
+ dispatch ( { type : "UPDATE_COUNTDOWN" , timeRemaining : remaining } ) ;
136
199
137
200
if ( remaining <= 0 ) {
138
201
if ( countdownRef . current ) {
@@ -154,20 +217,25 @@ export function useRetry(options: UseRetryOptions): UseRetryReturn {
154
217
// Effect to schedule next retry after a failed attempt
155
218
useEffect ( ( ) => {
156
219
if (
157
- ! isRetrying &&
158
- ! isManualRetry &&
159
- attemptCount > 0 &&
160
- attemptCount < maxAttempts
220
+ ! state . isRetrying &&
221
+ ! state . isManualRetry &&
222
+ state . attemptCount > 0 &&
223
+ state . attemptCount < maxAttempts
161
224
) {
162
- scheduleNextRetry ( attemptCount ) ;
225
+ scheduleNextRetry ( state . attemptCount ) ;
163
226
}
164
- } , [ attemptCount , isRetrying , isManualRetry , maxAttempts , scheduleNextRetry ] ) ;
227
+ } , [
228
+ state . attemptCount ,
229
+ state . isRetrying ,
230
+ state . isManualRetry ,
231
+ maxAttempts ,
232
+ scheduleNextRetry ,
233
+ ] ) ;
165
234
166
235
const retry = useCallback ( ( ) => {
167
- setIsManualRetry ( true ) ;
236
+ dispatch ( { type : "SET_MANUAL_RETRY" , isManual : true } ) ;
168
237
clearTimers ( ) ;
169
- setTimeUntilNextRetry ( null ) ;
170
- setCurrentDelay ( null ) ;
238
+ dispatch ( { type : "CANCEL_RETRY" } ) ;
171
239
performRetry ( ) ;
172
240
} , [ clearTimers , performRetry ] ) ;
173
241
@@ -178,11 +246,7 @@ export function useRetry(options: UseRetryOptions): UseRetryReturn {
178
246
179
247
const stopRetrying = useCallback ( ( ) => {
180
248
clearTimers ( ) ;
181
- setIsRetrying ( false ) ;
182
- setCurrentDelay ( null ) ;
183
- setAttemptCount ( 0 ) ;
184
- setTimeUntilNextRetry ( null ) ;
185
- setIsManualRetry ( false ) ;
249
+ dispatch ( { type : "RESET" } ) ;
186
250
} , [ clearTimers ] ) ;
187
251
188
252
// Cleanup on unmount
@@ -194,10 +258,10 @@ export function useRetry(options: UseRetryOptions): UseRetryReturn {
194
258
195
259
return {
196
260
retry,
197
- isRetrying,
198
- currentDelay,
199
- attemptCount,
200
- timeUntilNextRetry,
261
+ isRetrying : state . isRetrying ,
262
+ currentDelay : state . currentDelay ,
263
+ attemptCount : state . attemptCount ,
264
+ timeUntilNextRetry : state . timeUntilNextRetry ,
201
265
startRetrying,
202
266
stopRetrying,
203
267
} ;
0 commit comments