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
- New arch enabled.
- Mount a global
<Snackbar visible={visible} onDismiss={() => setVisible(false)} action={{ label: 'Action', onPress: () => {} }}>Message</Snackbar>.
setVisible(true), then setVisible(false).
- 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.
Summary
Under React Native's new architecture (Fabric), paper's
<Snackbar>can get stuck mounted at non-zero opacity aftervisibleis set tofalse. 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:Under Fabric, this
start({finished})callback fires withfinished: falseeven when nothing has interrupted the animation.setHidden(true)is the only thing that makes the component returnnulland actually unmount, so the snackbar stays rendered at whatever opacity the (visually-running) animation reached.The parallel show-path fix #4447 split
handleOnVisiblefromanimateShowto 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 (hiddenflips totrue) once the hide animation completes, regardless of thefinishedflag.Current behaviour
After
visible={false}:startcallback fires withfinished: false.setHidden(true)is skipped.setVisible(false)taps re-trigger the same broken animation; the snackbar never goes away.Repro
<Snackbar visible={visible} onDismiss={() => setVisible(false)} action={{ label: 'Action', onPress: () => {} }}>Message</Snackbar>.setVisible(true), thensetVisible(false).Reproduced on:
All with
newArchEnabled: true.Environment
react-native-paper: 5.15.2react-native: 0.83.6Proposed 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 newvisible=truepath →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.