Skip to content

fix: prune exiting digit slot when its fade animation is interrupted - #16

Open
saileshbro wants to merge 1 commit into
Rednegniw:mainfrom
saileshbro:fix/prune-exiting-slot-on-interrupted-animation
Open

fix: prune exiting digit slot when its fade animation is interrupted#16
saileshbro wants to merge 1 commit into
Rednegniw:mainfrom
saileshbro:fix/prune-exiting-slot-on-interrupted-animation

Conversation

@saileshbro

@saileshbro saileshbro commented Aug 7, 2026

Copy link
Copy Markdown

The bug

useSlotOpacity gates onExitComplete on finished === true:

(finished) => {
  "worklet";
  if (finished && onExitComplete && exitKey) {
    runOnJS(onExitComplete)(exitKey);
  }
},

An interrupted withTiming reports finished: false. useLayoutDiff keeps a key in exitingRef until onExitComplete fires for it, so when the fade is interrupted the key is never removed — the exiting glyph stays mounted on top of the live one indefinitely.

Reproducing

Anything that cancels the exit fade mid-flight:

  • change the value twice in quick succession, so a slot that is mid-exit is re-diffed
  • any re-render/teardown that cancels the animation while it is running — in our case tapping a card that pushes a formSheet over the screen while the digits were still rolling

Result: every slot that was mid-exit is left permanently doubled — the hero number, a secondary line and a percentage all rendering two superimposed digit stacks (2,238 over 2,276, and so on). It does not recover, because nothing else prunes exitingRef.

The fix

Report completion regardless of finished.

This is safe: useLayoutDiff already deletes any key present in the current layout during its diff —

for (const key of currentKeys) {
  exitingRef.current.delete(key);
}

— so a slot that has re-entered is never removed by this path. The callback only ever cleans up a slot that stopped animating, for whatever reason.

Notes

  • Verified on iOS (RN 0.83, Reanimated 4.5, native renderer) — the doubling was reproducible on demand before, and is gone after.
  • Added a patch changeset.
  • Happy to adjust the comment wording or split it if you would prefer a narrower guard (e.g. tracking cancellation explicitly rather than pruning on any non-finish).

`useSlotOpacity` gated `onExitComplete` on `finished === true`. An
interrupted `withTiming` reports `finished: false`, so the callback never
fired and `useLayoutDiff` kept the key in `exitingRef` indefinitely — the
exiting glyph stayed mounted on top of the live one.

Reproduces by changing the value twice in quick succession, or by any
re-render that cancels the fade mid-flight (e.g. a navigation push over the
screen): every digit slot that was mid-exit is left permanently doubled.

Pruning unconditionally is safe because `useLayoutDiff` already deletes any
key present in the current layout during its diff, so a slot that has
re-entered is never removed by this path.
Copilot AI lite review requested due to automatic review settings August 7, 2026 06:33
@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown

@saileshbro is attempting to deploy a commit to the antoninwingender's projects Team on Vercel.

A member of the Team first needs to authorize it.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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