Skip to content

fix(Snackbar): apply hidden state when animation reports finished:false under Fabric#5022

Open
momomuchu wants to merge 1 commit into
callstack:mainfrom
momomuchu:fix/snackbar-hide-fabric-finished-false
Open

fix(Snackbar): apply hidden state when animation reports finished:false under Fabric#5022
momomuchu wants to merge 1 commit into
callstack:mainfrom
momomuchu:fix/snackbar-hide-fabric-finished-false

Conversation

@momomuchu

Copy link
Copy Markdown

Summary

Fixes #4951 — the Snackbar stays mounted after visible becomes false under the New Architecture (Fabric), on iOS and Android.

Root cause

The hide animation gated the hidden state behind finished:

Animated.timing(opacity, { toValue: 0, ... }).start(({ finished }) => {
  if (finished) {
    setHidden(true);
  }
});

Under Fabric, Animated.timing can fire its start callback with { finished: false } even when the hide animation completes naturally (no interruption). The guard then skips setHidden(true), so the Snackbar never unmounts. This is the same class of Fabric callback noise that PR #4447 addressed for the show path (handleOnVisible); the hide path was missed.

Fix

Call setHidden(true) unconditionally in the hide animation callback, mirroring #4447.

Trade-off (documented in the commit): if visible flips back to true mid-hide, setHidden(true) fires but the next render immediately calls setHidden(false) via handleOnVisible — at most a one-frame snap, strictly better than a permanently stuck Snackbar that no state change can clear.

Test

Adds a regression test that mocks Animated.timing to invoke its callback with { finished: false } and asserts the Snackbar unmounts after visible becomes false. Full Snackbar.test.tsx suite: 7 passed.

Closes #4951

…d:false under Fabric — callstack#4951

Under the New Architecture (Fabric), `Animated.timing` can fire its
`start` callback with `{ finished: false }` even when the hide animation
completes naturally (no interruption). The previous code guarded
`setHidden(true)` behind `if (finished)`, so the Snackbar stayed
mounted forever on Fabric after `visible` became false.

The fix removes the guard and calls `setHidden(true)` unconditionally
in the animation callback, mirroring the show-path fix from PR callstack#4447
which addressed the same class of Fabric noise for `handleOnVisible`.

The sole trade-off is a mid-hide interrupt: if `visible` flips back to
`true` while the hide animation is running, `setHidden(true)` fires,
but the subsequent render immediately calls `setHidden(false)` via
`handleOnVisible`, causing at most a one-frame visual snap — strictly
better than a permanently stuck Snackbar that no state change can clear.

Adds a regression test that mocks `Animated.timing` to deliver
`{ finished: false }` and asserts the component unmounts cleanly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 2, 2026 09:35

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.

Pull request overview

Fixes a New Architecture (Fabric) Snackbar hide/unmount regression where the hide animation callback may report { finished: false }, preventing hidden from being set and leaving the component mounted after visible={false}.

Changes:

  • Updates the hide animation completion handling in Snackbar to ensure the hidden/unmount state is applied even when Fabric reports finished:false.
  • Adds a regression test that mocks Animated.timing to invoke its callback with { finished: false } and asserts the Snackbar unmounts.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/components/Snackbar.tsx Adjusts hide-animation completion behavior to prevent stuck-mounted Snackbar under Fabric.
src/components/tests/Snackbar.test.tsx Adds regression coverage for Fabric’s finished:false animation callback noise on the hide path.

Comment on lines 207 to 213
Animated.timing(opacity, {
toValue: 0,
duration: 100 * scale,
useNativeDriver: true,
}).start(({ finished }) => {
if (finished) {
setHidden(true);
}
}).start(() => {
setHidden(true);
});
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.

Snackbar stays mounted after visible=false under new architecture (iOS + Android)

2 participants