Menu component closes and fails to open again on re-render
Current behaviour
- Open the Menu component
- The parent component re-renders
- Menu component closes and can't be opened again
Expected behaviour
- Open the Menu component
- The parent component re-renders
- Menu component stays open
How to reproduce?
import React, {useState} from 'react';
import {View} from 'react-native';
import {Button, Menu, PaperProvider} from 'react-native-paper';
const MyComponent = () => {
const [randomVarialbeToForceRerender, setRandomVarialbeToForceRerender] =
useState(0);
const [visible, setVisible] = useState(false);
const openMenu = () => {
console.debug('Opening menu');
setVisible(true);
/**
* We update a state variable to force a re-render of the component.
* A real-world example could be a network request or some other
* asynchronous operation that updates the state.
*/
setTimeout(() => setRandomVarialbeToForceRerender(Math.random()), 1000);
};
const closeMenu = () => {
setVisible(false);
console.debug('Closing menu');
};
console.debug('visible', visible);
return (
<PaperProvider>
<View
style={{
paddingTop: 50,
flexDirection: 'row',
justifyContent: 'center',
}}>
<Menu
visible={visible}
onDismiss={closeMenu}
anchor={<Button onPress={openMenu}>Show menu</Button>}>
<Menu.Item
onPress={() => {}}
title={`Random ${randomVarialbeToForceRerender}`}
/>
</Menu>
</View>
</PaperProvider>
);
};
export default MyComponent;
Preview
https://github.com/user-attachments/assets/3d995026-ab4e-4fd3-bbf3-5544164a63ec
What have you tried so far?
Memoization to avoid re-rendering the menu. However, this approach is limited and breaks when the menu items are non-static (c.f. example)
Your Environment
| software | version |
|---|---|
| ios | 18.5 |
| android | x |
| react-native | 0.77.2 |
| react-native-paper | 5.13.1 |
| node | 18.20.8 |
| npm or yarn | 3.8.7 |
| expo sdk | x.x.x |
We ran into the same issue after upgrading to React Native 0.80 ("react-native-paper": "5.14.5") Only solution for now is to downgrade to React Native 0.79.4
https://github.com/user-attachments/assets/a7507580-89c9-4fa3-8cfe-923b7871381b
Same here with react-native 0.80.1
Also react-native 0.80.1
@julwil did you find any workaround ?
This seems to fix it:
diff --git a/node_modules/react-native-paper/src/components/Menu/Menu.tsx b/node_modules/react-native-paper/src/components/Menu/Menu.tsx
index 55922c1..3c9f295 100644
--- a/node_modules/react-native-paper/src/components/Menu/Menu.tsx
+++ b/node_modules/react-native-paper/src/components/Menu/Menu.tsx
@@ -20,6 +20,7 @@ import {
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import useLatestCallback from 'use-latest-callback';
import MenuItem from './MenuItem';
import { useInternalTheme } from '../../core/theming';
@@ -387,7 +390,7 @@ const Menu = ({
});
}, [removeListeners, theme]);
- const updateVisibility = React.useCallback(
+ const updateVisibility = useLatestCallback(
async (display: boolean) => {
// Menu is rendered in Portal, which updates items asynchronously
// We need to do the same here so that the ref is up-to-date
@@ -404,7 +407,6 @@ const Menu = ({
return;
});
},
- [hide, show]
);
React.useEffect(() => {
thank you @vpzomtrrfrt your solution works
Manually providing position coordinates didn't help either.
+import useLatestCallback from 'use-latest-callback';
Obviously would be good to get that merged in. But there's another issue, on the first try, it looks like the menu is opening 2 times. Or at least I see a weird duplicated animation. Not sure what to make of it
Any updates? Same issue with react-native 0.80.2 + react-native-paper 5.14.5. On the first try there's some flickering, feels like it is opening multiple times. After closing it, it can't be opened again.
UPD: Tried to experiment and downgraded react-native to 0.79.5 in a sample project. Now the menu seem to be working, but the issue with flickering on first try still persists.
I am experiencing this on RN 0.80.0 as well. I am hoping to not have to downgrade RN.
This seems to fix it:
diff --git a/node_modules/react-native-paper/src/components/Menu/Menu.tsx b/node_modules/react-native-paper/src/components/Menu/Menu.tsx index 55922c1..3c9f295 100644 --- a/node_modules/react-native-paper/src/components/Menu/Menu.tsx +++ b/node_modules/react-native-paper/src/components/Menu/Menu.tsx @@ -20,6 +20,7 @@ import { } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import useLatestCallback from 'use-latest-callback';
import MenuItem from './MenuItem'; import { useInternalTheme } from '../../core/theming'; @@ -387,7 +390,7 @@ const Menu = ({ }); }, [removeListeners, theme]);
- const updateVisibility = React.useCallback(
- const updateVisibility = useLatestCallback( async (display: boolean) => { // Menu is rendered in Portal, which updates items asynchronously // We need to do the same here so that the ref is up-to-date @@ -404,7 +407,6 @@ const Menu = ({ return; }); },
- [hide, show] );
React.useEffect(() => {
Be cautious with that. This works for static screens that don't change size, however on lists (Flatlist, ScrollView etc.) it makes the Menu not being calculated again for each item making it to appear wildly on the screen. For me it's not the solution, nor the workaround unfortunately.
EDIT: after playing with React Native Paper and React Native, I think it's more complex problem connected to RN itself. When used (I wanted to simulate similar component as Menu) simple Portal with menu and calculating the width and height with ref, it still behaves the same, e.g. renders not below / above the anchor but rather in totally different place. It looks to me like any next anchor on the list is displaying menu with previous anchor calculations.
tl;dr
this fix works, the problem is probably with the React Native, not RN Paper.
So I encountered this bug and have been trying to find out why it's failing. It turns out when you dismiss the menu and hide is called, the animation never actually finishes. Moving the prevRendered.current = false to before the Animation is set and started seemed to solve the problem for me.
However any logging for when finished is set on that anim never seems to show.
const hide = React.useCallback(() => {
removeListeners();
const { animation } = theme;
+ prevRendered.current = false;
Animated.timing(opacityAnimationRef.current, {
toValue: 0,
duration: ANIMATION_DURATION * animation.scale,
easing: EASING,
useNativeDriver: true,
}).start(({ finished }) => {
if (finished) {
setMenuLayout({ width: 0, height: 0 });
setRendered(false);
- prevRendered.current = false;
focusFirstDOMNode(anchorRef.current);
}
});
}, [removeListeners, theme]);
I hope this helps maybe resolve what the issue could be?
Looking at the RN documentation, the callback will always be called but finished is only true if the animation wasn't interrupted..
So we don't need to check if it finished or not before setting everything..
Working with animations Animations are started by calling start() on your animation. start() takes a completion callback that will be called when the animation is done. If the animation finished running normally, the completion callback will be invoked with {finished: true}. If the animation is done because stop() was called on it before it could finish (e.g. because it was interrupted by a gesture or another animation), then it will receive {finished: false}.
^^ from the RN documentation
+1, Experiencing the same issue
+1, Experiencing the same issue with latest version of react native with react native paper
"react-native-paper": "^5.14.5", "react": "19.1.0", "react-native": "0.81.0",
Opened a primary issue -> https://github.com/callstack/react-native-paper/issues/4797
This package has to be upgraded to React Native latest version, currently last version used was 0.77 within this package
@vpzomtrrfrt Thank for your work this have been the best patch so far. However, in order get it working in production and a release apk you have to change the CommonJS file (and maybe the ES Module just keep everything consistent) as @Bk49 mentions in https://github.com/callstack/react-native-paper/issues/4807#issuecomment-3316733192
The following negates the issue react-native-paper+5.14.5.patch
diff --git a/node_modules/react-native-paper/lib/commonjs/components/Menu/Menu.js b/node_modules/react-native-paper/lib/commonjs/components/Menu/Menu.js
index 0b9a9df..0a30df3 100644
--- a/node_modules/react-native-paper/lib/commonjs/components/Menu/Menu.js
+++ b/node_modules/react-native-paper/lib/commonjs/components/Menu/Menu.js
@@ -7,6 +7,7 @@ exports.default = exports.ELEVATION_LEVELS_MAP = void 0;
var React = _interopRequireWildcard(require("react"));
var _reactNative = require("react-native");
var _reactNativeSafeAreaContext = require("react-native-safe-area-context");
+var _useLatestCallback = _interopRequireDefault(require("use-latest-callback"));
var _MenuItem = _interopRequireDefault(require("./MenuItem"));
var _theming = require("../../core/theming");
var _types = require("../../types");
@@ -276,7 +277,7 @@ const Menu = ({
}
});
}, [removeListeners, theme]);
- const updateVisibility = React.useCallback(async display => {
+ const updateVisibility = (0, _useLatestCallback.default)(async display => {
// Menu is rendered in Portal, which updates items asynchronously
// We need to do the same here so that the ref is up-to-date
await Promise.resolve().then(() => {
@@ -289,7 +290,7 @@ const Menu = ({
}
return;
});
- }, [hide, show]);
+ });
React.useEffect(() => {
const opacityAnimation = opacityAnimationRef.current;
const scaleAnimation = scaleAnimationRef.current;
diff --git a/node_modules/react-native-paper/lib/module/components/Menu/Menu.js b/node_modules/react-native-paper/lib/module/components/Menu/Menu.js
index 46a45f4..0f0eb3e 100644
--- a/node_modules/react-native-paper/lib/module/components/Menu/Menu.js
+++ b/node_modules/react-native-paper/lib/module/components/Menu/Menu.js
@@ -2,6 +2,7 @@ function _extends() { return _extends = Object.assign ? Object.assign.bind() : f
import * as React from 'react';
import { Animated, Dimensions, Easing, I18nManager, Keyboard, Platform, ScrollView, StyleSheet, View, Pressable } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import useLatestCallback from 'use-latest-callback';
import MenuItem from './MenuItem';
import { useInternalTheme } from '../../core/theming';
import { ElevationLevels } from '../../types';
@@ -268,7 +269,7 @@ const Menu = ({
}
});
}, [removeListeners, theme]);
- const updateVisibility = React.useCallback(async display => {
+ const updateVisibility = useLatestCallback(async display => {
// Menu is rendered in Portal, which updates items asynchronously
// We need to do the same here so that the ref is up-to-date
await Promise.resolve().then(() => {
@@ -281,7 +282,7 @@ const Menu = ({
}
return;
});
- }, [hide, show]);
+ });
React.useEffect(() => {
const opacityAnimation = opacityAnimationRef.current;
const scaleAnimation = scaleAnimationRef.current;
diff --git a/node_modules/react-native-paper/src/components/Menu/Menu.tsx b/node_modules/react-native-paper/src/components/Menu/Menu.tsx
index 55922c1..c2b2365 100644
--- a/node_modules/react-native-paper/src/components/Menu/Menu.tsx
+++ b/node_modules/react-native-paper/src/components/Menu/Menu.tsx
@@ -20,6 +20,7 @@ import {
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import useLatestCallback from 'use-latest-callback';
import MenuItem from './MenuItem';
import { useInternalTheme } from '../../core/theming';
@@ -387,7 +388,7 @@ const Menu = ({
});
}, [removeListeners, theme]);
- const updateVisibility = React.useCallback(
+ const updateVisibility = useLatestCallback(
async (display: boolean) => {
// Menu is rendered in Portal, which updates items asynchronously
// We need to do the same here so that the ref is up-to-date
@@ -403,8 +404,7 @@ const Menu = ({
return;
});
- },
- [hide, show]
+ }
);
React.useEffect(() => {
Duplicate issues that maybe find this helpful: https://github.com/callstack/react-native-paper/issues/4807 https://github.com/callstack/react-native-paper/issues/4814 https://github.com/callstack/react-native-paper/issues/4812 https://github.com/callstack/react-native-paper/issues/4797
@shockdesign Thank u for the fix!
+1
@shockdesign Your fix has worked for me. Maybe create a PR?
Any progressing?
Just to keep everything here. Two solutions that seem to work: https://github.com/callstack/react-native-paper/issues/4797#issuecomment-3597267794 https://github.com/callstack/react-native-paper/issues/4763#issuecomment-3427895632
Here a solution I found, I don't know if can help someone (key={Number(menuVisible)}):
<Menu
key={Number(menuVisible)}
anchor={
<IconButton
icon="dots-vertical"
onPress={() => {
setMenuVisible(true);
}}
/>
}
onDismiss={() => {
setMenuVisible(false);
}}
visible={menuVisible}
>
<Menu.Item onPress={() => {}} title="Item 1" />
<Menu.Item onPress={() => {}} title="Item 2" />
<Divider />
<Menu.Item onPress={() => {}} title="Item 3" />
</Menu>