Skip to content

fix(useInterval): clear a manually resumed interval on unmount - #212

Merged
childrentime merged 1 commit into
childrentime:mainfrom
ostapondo:fix/interval-manual-controls-unmount
Jul 30, 2026
Merged

fix(useInterval): clear a manually resumed interval on unmount#212
childrentime merged 1 commit into
childrentime:mainfrom
ostapondo:fix/interval-manual-controls-unmount

Conversation

@ostapondo

Copy link
Copy Markdown
Contributor

Description

useInterval never clears its timer on unmount when controls: true is used.

The effect that owns the cleanup bails out before it can register one:

if (controls) {
  return
}

So an interval started through resume() keeps firing after the component is
gone. The hook's docs promise the opposite:

The interval is automatically cleared on component unmount.

To reproduce: mount with { controls: true }, call resume(), unmount, then
advance the timers. The callback keeps running — 10 more calls over 500ms at a
50ms delay in the test I added.

I put the unmount cleanup in its own mount-scoped effect rather than returning
clean from the existing one. That keeps the current delay-change behaviour
untouched: in manual mode, updating delay still doesn't stop a running timer,
which felt like a separate decision from fixing the leak.

While I was in there — resume() overwrote timer.current without clearing the
previous timer, so calling it twice orphaned the first interval and neither
pause() nor the unmount cleanup could reach it afterwards. It now clears first.

Type of Change

  • Bug fix
  • New hook
  • Enhancement to existing hook
  • Documentation update
  • Other (please describe)

Checklist

  • I have read the Contributing Guide
  • My code follows the project's coding style
  • I have added tests for my changes
  • All existing tests pass
  • I have updated the documentation

No doc change: the existing page already describes the fixed behaviour.

Two tests added to useInterval/index.spec.ts — one for the unmount leak, one
for the double-resume() orphan. Both fail on main and pass here; the other 9
in that file are untouched.

With `controls: true` the effect returns before it registers a cleanup,
so an interval started through resume() keeps firing after the component
unmounts - the hook docs already state the interval is cleared on
unmount. Register the cleanup in its own mount-scoped effect so it runs
in both modes without changing what happens when `delay` updates.

resume() also overwrote timer.current without clearing the previous
timer, so calling it twice left an interval that neither pause() nor the
unmount cleanup could reach. Clear before starting a new one.
@childrentime
childrentime merged commit 8710fff into childrentime:main Jul 30, 2026
5 checks passed
@childrentime

Copy link
Copy Markdown
Owner

Thanks @ostapondo! Solid fix — the docs promised cleanup on unmount and the controls path just never delivered it. Catching the double-resume() orphan interval on the way through was a great extra, and I appreciate how deliberately you scoped this (leaving the delay-change semantics as a separate decision). Merged 🎉

childrentime added a commit that referenced this pull request Jul 31, 2026
Fixes
- useOrientation: lockOrientation/unlockOrientation had inverted isBrowser
  guards — they early-returned in the browser and only ran during SSR,
  making both no-ops where they mattered. Closes #215.
- useInterval: with controls: true, an interval started through resume()
  kept firing after unmount, and calling resume() twice leaked a timer
  neither pause() nor unmount could reach. Closes #212.
- useMicrophone: level stayed frozen at the last reading after stop();
  it now resets to 0. Closes #213.
- useElementByPoint: multiple mode re-rendered on every rAF frame because
  elementsFromPoint allocates a fresh array; the hit list is now compared
  element-by-element and kept stable when unchanged. Closes #214.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants