diff --git a/src/hooks/useSmartPolling.test.tsx b/src/hooks/useSmartPolling.test.tsx index af0e3ea..9c0a3b8 100644 --- a/src/hooks/useSmartPolling.test.tsx +++ b/src/hooks/useSmartPolling.test.tsx @@ -50,6 +50,27 @@ describe("useSmartPolling", () => { await waitFor(() => expect(fetchFn).toHaveBeenCalledTimes(2)); }); + it("keeps polling across re-renders when callbacks are inline (#568)", async () => { + const ref = { count: 0 }; + const base = createFetchFn([{ n: 1 }, { n: 2 }, { n: 3 }, { n: 4 }], ref); + renderHook(() => + useSmartPolling({ + fetchFn: () => base(), + compareFn: (a, b) => JSON.stringify(a) === JSON.stringify(b), + onDataChange: () => {}, + interval: 1000, + }), + ); + + await waitFor(() => expect(base).toHaveBeenCalledTimes(1)); + for (let i = 0; i < 3; i++) { + await act(async () => { + jest.advanceTimersByTime(1000); + }); + } + await waitFor(() => expect(base.mock.calls.length).toBeGreaterThan(2)); + }); + it("applies backoff after unchangedThreshold identical results", async () => { const ref = { count: 0 }; // Always return the same value → compareFn says "unchanged" diff --git a/src/hooks/useSmartPolling.ts b/src/hooks/useSmartPolling.ts index 81507fc..7a4a7bb 100644 --- a/src/hooks/useSmartPolling.ts +++ b/src/hooks/useSmartPolling.ts @@ -73,17 +73,26 @@ export function useSmartPolling({ const previousDataRef = useRef(null); const isBackgroundedRef = useRef(false); + // Keep callbacks in refs so callers passing inline functions don't change + // fetchData's identity (which would tear down the polling effect each render). + const fetchFnRef = useRef(fetchFn); + const compareFnRef = useRef(compareFn); + const onDataChangeRef = useRef(onDataChange); + fetchFnRef.current = fetchFn; + compareFnRef.current = compareFn; + onDataChangeRef.current = onDataChange; + const fetchData = useCallback(async () => { if (!isMountedRef.current) return; - + try { setIsLoading(true); - const result = await fetchFn(); + const result = await fetchFnRef.current(); if (!isMountedRef.current) return; const hasChanged = previousDataRef.current !== null - ? !compareFn(previousDataRef.current, result) + ? !compareFnRef.current(previousDataRef.current, result) : true; if (hasChanged) { @@ -92,7 +101,7 @@ export function useSmartPolling({ unchangedCountRef.current = 0; currentIntervalRef.current = interval; dispatch({ type: 'STOP_BACKOFF' }); - onDataChange?.(result); + onDataChangeRef.current?.(result); } else { unchangedCountRef.current += 1; @@ -118,7 +127,7 @@ export function useSmartPolling({ setIsLoading(false); } } - }, [fetchFn, compareFn, interval, backoffMultiplier, maxBackoff, unchangedThreshold, onDataChange]); + }, [interval, backoffMultiplier, maxBackoff, unchangedThreshold]); const refetch = useCallback(async () => { currentIntervalRef.current = interval; @@ -154,9 +163,7 @@ export function useSmartPolling({ }; }, [enabled, fetchData]); - // Main polling effect - use a ref to track initial mount - const hasInitialized = useRef(false); - + // Main polling effect: (re)creates the interval whenever it re-runs useEffect(() => { isMountedRef.current = true; @@ -164,13 +171,9 @@ export function useSmartPolling({ return; } - // Only start polling on initial mount or when enabled changes - if (!hasInitialized.current) { - hasInitialized.current = true; - dispatch({ type: 'START_POLLING' }); - fetchData(); - intervalRef.current = setInterval(fetchData, currentIntervalRef.current); - } + dispatch({ type: 'START_POLLING' }); + fetchData(); + intervalRef.current = setInterval(fetchData, currentIntervalRef.current); return () => { isMountedRef.current = false;