callstack / callstack/react-native-paper

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

Open
#4,951 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.