From 736b12e1ae51df50d1d0ef3aa6cd49a10e8ac84a Mon Sep 17 00:00:00 2001 From: Rene Date: Tue, 15 Sep 2026 05:12:50 +0200 Subject: [PATCH] fix(hooks): reset loading state to false on successful execution in useRetry (#4) --- src/hooks/useRetry.test.ts | 57 ++++++++++++++++++++++++++++++++++++++ src/hooks/useRetry.ts | 40 +++++++++++++++++++------- 2 files changed, 87 insertions(+), 10 deletions(-) create mode 100644 src/hooks/useRetry.test.ts diff --git a/src/hooks/useRetry.test.ts b/src/hooks/useRetry.test.ts new file mode 100644 index 0000000..954b608 --- /dev/null +++ b/src/hooks/useRetry.test.ts @@ -0,0 +1,57 @@ +import { act, renderHook } from "@testing-library/react" +import { describe, it, expect, vi } from "vitest" +import { useRetry } from "./useRetry" + +describe("useRetry", () => { + it("resets loading to false after successful execution on first attempt (#4)", async () => { + const fn = vi.fn().mockResolvedValue(undefined) + const { result } = renderHook(() => useRetry(fn)) + + expect(result.current.loading).toBe(false) + + await act(async () => { + await result.current.run() + }) + + expect(result.current.loading).toBe(false) + expect(result.current.attempt).toBe(0) + expect(result.current.error).toBeNull() + expect(fn).toHaveBeenCalledTimes(1) + }) + + it("resets loading to false after successful execution on retry (#4)", async () => { + const fn = vi + .fn() + .mockRejectedValueOnce(new Error("Transient error")) + .mockResolvedValue(undefined) + + const { result } = renderHook(() => + useRetry(fn, { maxAttempts: 3, delay: 10 }) + ) + + await act(async () => { + await result.current.run() + }) + + expect(result.current.loading).toBe(false) + expect(result.current.attempt).toBe(0) + expect(result.current.error).toBeNull() + expect(fn).toHaveBeenCalledTimes(2) + }) + + it("resets loading to false and sets error message when all attempts fail", async () => { + const fn = vi.fn().mockRejectedValue(new Error("Persistent failure")) + const { result } = renderHook(() => + useRetry(fn, { maxAttempts: 2, delay: 10 }) + ) + + await act(async () => { + await result.current.run() + }) + + expect(result.current.loading).toBe(false) + expect(result.current.attempt).toBe(2) + expect(result.current.error).toBe("Persistent failure") + expect(fn).toHaveBeenCalledTimes(2) + }) +}) diff --git a/src/hooks/useRetry.ts b/src/hooks/useRetry.ts index 6a030d9..e712b22 100644 --- a/src/hooks/useRetry.ts +++ b/src/hooks/useRetry.ts @@ -1,20 +1,40 @@ import { useState, useCallback } from "react" -interface Options { maxAttempts?: number; delay?: number } -export function useRetry(fn: () => Promise, { maxAttempts = 3, delay = 1000 }: Options = {}) { + +interface Options { + maxAttempts?: number + delay?: number +} + +export function useRetry( + fn: () => Promise, + { maxAttempts = 3, delay = 1000 }: Options = {} +) { const [attempt, setAttempt] = useState(0) const [error, setError] = useState(null) const [loading, setLoading] = useState(false) + const run = useCallback(async () => { - setLoading(true); setError(null) - for (let i = 0; i < maxAttempts; i++) { - try { await fn(); setAttempt(0); return } - catch (e) { - setAttempt(i + 1) - if (i < maxAttempts - 1) await new Promise(r => setTimeout(r, delay * (i + 1))) - else setError((e as Error).message) + setLoading(true) + setError(null) + try { + for (let i = 0; i < maxAttempts; i++) { + try { + await fn() + setAttempt(0) + return + } catch (e) { + setAttempt(i + 1) + if (i < maxAttempts - 1) { + await new Promise((r) => setTimeout(r, delay * (i + 1))) + } else { + setError((e as Error).message) + } + } } + } finally { + setLoading(false) } - setLoading(false) }, [fn, maxAttempts, delay]) + return { run, attempt, error, loading } }