diff --git a/packages/core/src/useInterval/index.spec.ts b/packages/core/src/useInterval/index.spec.ts index 9ed0dd82..02519376 100644 --- a/packages/core/src/useInterval/index.spec.ts +++ b/packages/core/src/useInterval/index.spec.ts @@ -125,4 +125,35 @@ describe('useInterval', () => { jest.advanceTimersByTime(70) expect(callback).toHaveBeenCalledTimes(1) }) + + it('should clear a manually resumed interval on unmount', () => { + const callback = jest.fn() + const { result, unmount } = renderHook(() => + useInterval(callback, 50, { controls: true }), + ) + + result.current.resume() + jest.advanceTimersByTime(50) + expect(callback).toHaveBeenCalledTimes(1) + + unmount() + callback.mockClear() + + jest.advanceTimersByTime(500) + expect(callback).toHaveBeenCalledTimes(0) + }) + + it('should not leave an orphan interval when resume is called twice', () => { + const callback = jest.fn() + const { result } = renderHook(() => + useInterval(callback, 50, { controls: true }), + ) + + result.current.resume() + result.current.resume() + result.current.pause() + + jest.advanceTimersByTime(500) + expect(callback).toHaveBeenCalledTimes(0) + }) }) diff --git a/packages/core/src/useInterval/index.ts b/packages/core/src/useInterval/index.ts index cc5242fc..a8d1e265 100644 --- a/packages/core/src/useInterval/index.ts +++ b/packages/core/src/useInterval/index.ts @@ -1,6 +1,7 @@ import { useEffect, useRef } from 'react' import { useEvent } from '../useEvent' import { useLatest } from '../useLatest' +import { useUnmount } from '../useUnmount' import { defaultOptions } from '../utils/defaults' import type { UseInterval } from './interface' @@ -18,10 +19,16 @@ export const useInterval: UseInterval = ( const timer = useRef | null>(null) const clean = () => { - timer.current && clearInterval(timer.current) + if (timer.current) { + clearInterval(timer.current) + timer.current = null + } } const resume = useEvent(() => { + // Drop a running timer first — otherwise its id is overwritten below and + // neither pause() nor unmount can reach it again. + clean() isActive.current = true timer.current = setInterval(() => savedCallback.current(), delay || 0) }) @@ -49,6 +56,11 @@ export const useInterval: UseInterval = ( // eslint-disable-next-line react-hooks/exhaustive-deps }, [delay, immediate]) + // The effect above bails out before registering a cleanup when `controls` is + // set, so a timer started by resume() would outlive the component. Clear it on + // unmount whichever mode the hook runs in. + useUnmount(clean) + return { isActive, pause,