Skip to content

Commit dd7adda

Browse files
refactor: consolidate useRetry state with useReducer
Convert useRetry hook from multiple useState calls to a single useReducer for cleaner state management. This improves code clarity and makes state transitions more predictable. Changes: - Replace 5 useState calls with single useReducer - Add RetryState interface and RetryAction union type - Implement retryReducer function for all state transitions - Update all state access to use state object - Replace setState calls with dispatch calls throughout Co-authored-by: BrunoQuaresma <3165839+BrunoQuaresma@users.noreply.github.com>
1 parent d398265 commit dd7adda

File tree

1 file changed

+104
-40
lines changed

1 file changed

+104
-40
lines changed

site/src/hooks/useRetry.ts

Lines changed: 104 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useEffect, useRef, useState } from "react";
1+
import { useCallback, useEffect, useReducer, useRef } from "react";
22
import { useEffectEvent } from "./hookPolyfills";
33

44
interface UseRetryOptions {
@@ -55,18 +55,89 @@ interface UseRetryReturn {
5555
stopRetrying: () => void;
5656
}
5757

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+
58135
/**
59136
* Hook for handling exponential backoff retry logic
60137
*/
61138
export function useRetry(options: UseRetryOptions): UseRetryReturn {
62139
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);
70141

71142
const timeoutRef = useRef<number | null>(null);
72143
const countdownRef = useRef<number | null>(null);
@@ -95,23 +166,16 @@ export function useRetry(options: UseRetryOptions): UseRetryReturn {
95166
);
96167

97168
const performRetry = useCallback(async () => {
98-
setIsRetrying(true);
99-
setTimeUntilNextRetry(null);
100-
setCurrentDelay(null);
169+
dispatch({ type: "START_RETRY" });
101170
clearTimers();
102-
// Increment attempt count when starting the retry
103-
setAttemptCount((prev) => prev + 1);
104171

105172
try {
106173
await onRetryEvent();
107174
// If retry succeeds, reset everything
108-
setAttemptCount(0);
109-
setIsRetrying(false);
110-
setIsManualRetry(false);
175+
dispatch({ type: "RETRY_SUCCESS" });
111176
} 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" });
115179
}
116180
}, [onRetryEvent, clearTimers]);
117181

@@ -123,16 +187,15 @@ export function useRetry(options: UseRetryOptions): UseRetryReturn {
123187

124188
// Calculate delay based on attempt - 1 (so first retry gets initialDelay)
125189
const delay = calculateDelay(Math.max(0, attempt - 1));
126-
setCurrentDelay(delay);
127-
setTimeUntilNextRetry(delay);
190+
dispatch({ type: "SCHEDULE_RETRY", delay });
128191
startTimeRef.current = Date.now();
129192

130193
// Start countdown timer
131194
countdownRef.current = window.setInterval(() => {
132195
if (startTimeRef.current) {
133196
const elapsed = Date.now() - startTimeRef.current;
134197
const remaining = Math.max(0, delay - elapsed);
135-
setTimeUntilNextRetry(remaining);
198+
dispatch({ type: "UPDATE_COUNTDOWN", timeRemaining: remaining });
136199

137200
if (remaining <= 0) {
138201
if (countdownRef.current) {
@@ -154,20 +217,25 @@ export function useRetry(options: UseRetryOptions): UseRetryReturn {
154217
// Effect to schedule next retry after a failed attempt
155218
useEffect(() => {
156219
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
161224
) {
162-
scheduleNextRetry(attemptCount);
225+
scheduleNextRetry(state.attemptCount);
163226
}
164-
}, [attemptCount, isRetrying, isManualRetry, maxAttempts, scheduleNextRetry]);
227+
}, [
228+
state.attemptCount,
229+
state.isRetrying,
230+
state.isManualRetry,
231+
maxAttempts,
232+
scheduleNextRetry,
233+
]);
165234

166235
const retry = useCallback(() => {
167-
setIsManualRetry(true);
236+
dispatch({ type: "SET_MANUAL_RETRY", isManual: true });
168237
clearTimers();
169-
setTimeUntilNextRetry(null);
170-
setCurrentDelay(null);
238+
dispatch({ type: "CANCEL_RETRY" });
171239
performRetry();
172240
}, [clearTimers, performRetry]);
173241

@@ -178,11 +246,7 @@ export function useRetry(options: UseRetryOptions): UseRetryReturn {
178246

179247
const stopRetrying = useCallback(() => {
180248
clearTimers();
181-
setIsRetrying(false);
182-
setCurrentDelay(null);
183-
setAttemptCount(0);
184-
setTimeUntilNextRetry(null);
185-
setIsManualRetry(false);
249+
dispatch({ type: "RESET" });
186250
}, [clearTimers]);
187251

188252
// Cleanup on unmount
@@ -194,10 +258,10 @@ export function useRetry(options: UseRetryOptions): UseRetryReturn {
194258

195259
return {
196260
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,
201265
startRetrying,
202266
stopRetrying,
203267
};

0 commit comments

Comments
 (0)