callstack / callstack/react-native-paper
Snackbar stays mounted after `visible=false` under new architecture (iOS + Android)
- Dominant language
- TypeScript
- Stars
- 14.5k
- Forks
- 2.2k
- Avg merge
- 5d 23h
- Merged PRs (30d)
- 12
Description
## Summary
Under React Native's new architecture (Fabric), paper's `` 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`](https://github.com/callstack/react-native-paper/blob/main/src/components/Snackbar.tsx#L219-L223), `handleOnHidden`:
```tsx
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 ` setVisible(false)} action={{ label: 'Action', onPress: () => {} }}>Message`.
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:
```diff
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.
Contributor guide
Assessment
This issue has not been assessed yet.