Skip to content

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

@dobomode

Description

@dobomode

Summary

Under React Native's new architecture (Fabric), paper's <Snackbar> can get stuck mounted at non-zero opacity after visible is set to false. The hide animation runs visually, but the component never unmounts and stays visible on screen until something else forces a remount.

Sister bug to #3775 / #4445 (which were Android-only show path, fixed by #4447). This is the hide path, and reproduces on both iOS and Android with the new arch enabled.

Root cause

src/components/Snackbar.tsx, handleOnHidden:

Animated.timing(opacity, {
  toValue: 0,
  duration: 100 * scale,
  useNativeDriver: true,
}).start(({ finished }) => {
  if (finished) {       // ← this guard is the problem under Fabric
    setHidden(true);
  }
});

Under Fabric, this start({finished}) callback fires with finished: false even when nothing has interrupted the animation. setHidden(true) is the only thing that makes the component return null and actually unmount, so the snackbar stays rendered at whatever opacity the (visually-running) animation reached.

The parallel show-path fix #4447 split handleOnVisible from animateShow to work around the same Fabric noise. The hide path was not touched and still carries the guard.

Expected behaviour

After visible={false}, the Snackbar component unmounts cleanly (hidden flips to true) once the hide animation completes, regardless of the finished flag.

Current behaviour

After visible={false}:

  • The hide animation fires (opacity 1 → 0).
  • The start callback fires with finished: false.
  • setHidden(true) is skipped.
  • The component stays mounted; nothing in the React state can clear it.
  • Subsequent setVisible(false) taps re-trigger the same broken animation; the snackbar never goes away.

Repro

  1. New arch enabled.
  2. Mount a global <Snackbar visible={visible} onDismiss={() => setVisible(false)} action={{ label: 'Action', onPress: () => {} }}>Message</Snackbar>.
  3. setVisible(true), then setVisible(false).
  4. Snackbar stays on screen.

Reproduced on:

  • iOS 18 simulator (iPhone 15)
  • iOS 26 simulator (iPhone 17 Pro Max)
  • Android Pixel 8 emulator (API 35)

All with newArchEnabled: true.

Environment

  • react-native-paper: 5.15.2
  • react-native: 0.83.6
  • Expo SDK 55
  • Fabric / new arch: enabled

Proposed fix

Drop the if (finished) guard, mirroring how #4447 worked around the same class of Fabric noise on the show path:

   Animated.timing(opacity, {
     toValue: 0,
     duration: 100 * scale,
     useNativeDriver: true,
-  }).start(({ finished }) => {
-    if (finished) {
-      setHidden(true);
-    }
+  }).start(() => {
+    setHidden(true);
   });

Trade-off

Under a genuine interrupt (a new show kicked off mid-hide), setHidden(true) would now also fire. The next render uses the new visible=true path → setHidden(false) → re-animates show. The visible artefact is at worst a one-frame snap. A stuck Snackbar that no state change can clear is strictly worse.

Happy to open a PR if useful. Tested locally via patch-package; resolves the issue on both platforms.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions