[Bug]: Bottom sheet does not always open when React's Strict Mode is enabled
Version
v5
Reanimated Version
v3
Gesture Handler Version
v2
Platforms
iOS, Android
What happened?
When React's Strict Mode is enabled, the bottom sheet does not always open.
This is unrelated β they are thrown at the same time but do not cause the issue described here. ~The following errors are also thrown, which may be related (more details in https://github.com/software-mansion/react-native-reanimated/issues/6997):~
ERROR Warning: findHostInstance_DEPRECATED is deprecated in StrictMode. findHostInstance_DEPRECATED
was passed an instance of AnimatedComponent(Wrap) which is inside StrictMode. Instead, add a
ref directly to the element you want to reference. Learn more about using refs safely here:
https://react.dev/link/strict-mode-find-node
ERROR Warning: findNodeHandle is deprecated in StrictMode. findNodeHandle was passed an instance of
Wrap which is inside StrictMode. Instead, add a ref directly to the element you want to reference.
Learn more about using refs safely here: https://react.dev/link/strict-mode-find-node
https://github.com/user-attachments/assets/676abe68-94c2-43a8-91ab-c8ce32eba26b
Reproduction steps
Using reproduction repository
- Clone the repository.
- Install dependencies (
yarn install). - Build and run the app (e.g.
yarn ios). - Press on button to open bottom sheet modal.
The bottom sheet modal does not open (most of the time). findHostInstance_DEPRECATED is deprecated in StrictMode and findNodeHandle is deprecated in StrictMode errors are reported.
Self-reproduction
Enable React's Strict Mode (wrap app components with <StrictMode>...</StrictMode>).
Reproduction sample
https://github.com/winghouchan/react-native-bottom-sheet-strict-mode-mcve
Ignore as Snack is broken, see #2131, but issue needs it other it will be auto-closed
https://snack.expo.dev/@gorhom/bottom-sheet---issue-reproduction-template
Relevant log output
LOG [BottomSheetContainer::handleContainerLayout] height:852
LOG [handlePresent]
LOG [handlePortalOnUnmount] minimized:false forcedDismissed:false
ERROR Warning: findHostInstance_DEPRECATED is deprecated in StrictMode. findHostInstance_DEPRECATED was passed an instance of AnimatedComponent(Wrap) which is inside StrictMode. Instead, add a ref directly to the element you want to reference. Learn more about using refs safely here: https://react.dev/link/strict-mode-find-node
ERROR Warning: findNodeHandle is deprecated in StrictMode. findNodeHandle was passed an instance of Wrap which is inside StrictMode. Instead, add a ref directly to the element you want to reference. Learn more about using refs safely here: https://react.dev/link/strict-mode-find-node
LOG [BottomSheetView::handleLayout] height:426
LOG [BottomSheetHandleContainer::handleContainerLayout] height:24
Hi @winghouchan , this issue is coming from Reanimated and it seems resolved in their package https://github.com/software-mansion/react-native-reanimated/pull/6736
Hi @gorhom! I don't think it's solved with the linked PR. The changes there were released in 3.16.5. The reproduction uses 3.16.7 (reference). Also see the new issue I've opened in their repo (https://github.com/software-mansion/react-native-reanimated/issues/6997).
https://github.com/user-attachments/assets/3c0a9957-b8e9-40c1-a926-8eff9c25624e
Interestingly, the bottom sheet is attached to the component tree but it isn't displayed on screen. Observe that the PortalHost has no children initially but when the "Present Modal" button is pressed, it does have children but the bottom sheet is not displayed.
https://github.com/user-attachments/assets/a8815a36-688b-4848-a535-8e3bd8e2be8d
Another observation: the modal can be opened in Strict Mode if:
- Strict Mode was initially disabled.
- While Strict Mode was initially disabled, the modal was opened.
- Strict Mode is then disabled and the app's JavaScript bundle is not hard refreshed.
Important points:
- If the modal wasn't opened while Strict Mode was disabled (point 2 above), the modal doesn't appear to open after Strict Mode is enabled and changes are introduced via Fast Refresh.
- If the app's JavaScript bundle was hard refreshed after Strict Mode is enabled (point 3 above), the modal doesn't appear to open.
Found there's a logger π
Here are the logs:
LOG [BottomSheetContainer::handleContainerLayout] height:852
LOG [handlePresent]
LOG [handlePortalOnUnmount] minimized:false forcedDismissed:false
ERROR Warning: findHostInstance_DEPRECATED is deprecated in StrictMode. findHostInstance_DEPRECATED was passed an instance of AnimatedComponent(Wrap) which is inside StrictMode. Instead, add a ref directly to the element you want to reference. Learn more about using refs safely here: https://react.dev/link/strict-mode-find-node
ERROR Warning: findNodeHandle is deprecated in StrictMode. findNodeHandle was passed an instance of Wrap which is inside StrictMode. Instead, add a ref directly to the element you want to reference. Learn more about using refs safely here: https://react.dev/link/strict-mode-find-node
LOG [BottomSheetView::handleLayout] height:426
LOG [BottomSheetHandleContainer::handleContainerLayout] height:24
Interesting that the portal's unmount handler is run. Suspect this is a consequence of Strict Mode running Effects twice.
For comparison, these are the logs when Strict Mode is disabled:
LOG [BottomSheetContainer::handleContainerLayout] height:852
LOG [handlePresent]
LOG [BottomSheetView::handleLayout] height:426
LOG [BottomSheetHandleContainer::handleContainerLayout] height:24
LOG [fun::useAnimatedReaction::OnSnapPointChange] result:426
LOG [fun::bound animateToPosition_Gorhom_BottomSheetTsx12] currentPosition:852 nextPosition:426 source:1
LOG [BottomSheet::handleOnAnimate] toIndex:0 fromIndex:-1
LOG [fun::bound animateToPositionCompleted_Gorhom_BottomSheetTsx11] animatedCurrentIndex:-1 animatedNextPosition:426 animatedNextPositionIndex:0
LOG [fun::useAnimatedReaction::OnChange] animatedCurrentIndex:-1 animatedIndex:0
LOG [BottomSheet::handleOnChange] index:0 animatedCurrentIndex:0
LOG [handleBottomSheetOnChange] minimized:false forcedDismissed:false
Based on the first difference between the logs, I decided to take a look at the function where [fun::useAnimatedReaction::OnSnapPointChange] result:426 is logged: https://github.com/gorhom/react-native-bottom-sheet/blob/e5d8aba49de1788ec309698a7170c2d3cc2eef4d/src/components/bottomSheet/BottomSheet.tsx#L1546-L1581
Looking at the value of variables while this function is run, isLayoutCalculated.value has a value of false, which results in an early return: https://github.com/gorhom/react-native-bottom-sheet/blob/e5d8aba49de1788ec309698a7170c2d3cc2eef4d/src/components/bottomSheet/BottomSheet.tsx#L1560-L1565
This stops the log occurring and also stops the running of evaluatePosition: https://github.com/gorhom/react-native-bottom-sheet/blob/e5d8aba49de1788ec309698a7170c2d3cc2eef4d/src/components/bottomSheet/BottomSheet.tsx#L1567-L1578
evaluatePosition is significant because it determines the position of the bottom sheet β if it is not run, the sheet doesn't get positioned on the screen: https://github.com/gorhom/react-native-bottom-sheet/blob/e5d8aba49de1788ec309698a7170c2d3cc2eef4d/src/components/bottomSheet/BottomSheet.tsx#L901-L1022
Next step is to understand why isLayoutCalculated.value has a value of false.
isLayoutCalculated is derived as follows: https://github.com/gorhom/react-native-bottom-sheet/blob/e5d8aba49de1788ec309698a7170c2d3cc2eef4d/src/components/bottomSheet/BottomSheet.tsx#L255-L297
In my observations, the values for the three values that determine isLayoutCalculated are:
isContainerHeightCalculated: trueisHandleHeightCalculated: falseisSnapPointsNormalized : true
isHandleHeightCalculated is false because the conditions check if handleComponent is null or if animatedHandleHeight is not the initial height, while handleComponent will be undefined if not set: https://github.com/gorhom/react-native-bottom-sheet/blob/e5d8aba49de1788ec309698a7170c2d3cc2eef4d/src/components/bottomSheet/BottomSheet.tsx#L269-L278
Setting the handleComponent prop on BottomSheetModal seems to resolve the issue β but obviously not acceptable unless you want to get rid of the handle component.
Another interesting observation is if the debugger is paused for long enough, isHandleHeightCalculated and therefore isLayoutCalculated get resolved to true, allowing for the evaluatePosition function to be run and the sheet displayed on screen.
So BottomSheet passes animatedHandleHeight to BottomSheetHandleContainer: https://github.com/gorhom/react-native-bottom-sheet/blob/e5d8aba49de1788ec309698a7170c2d3cc2eef4d/src/components/bottomSheet/BottomSheet.tsx#L1925
BottomSheetHandleContainer then sets a value to animatedHandleHeight on layout: https://github.com/gorhom/react-native-bottom-sheet/blob/e5d8aba49de1788ec309698a7170c2d3cc2eef4d/src/components/bottomSheetHandleContainer/BottomSheetHandleContainer.tsx#L109-L129
This is observed in both Strict Mode enabled and disabled cases as the following is logged:
LOG [BottomSheetHandleContainer::handleContainerLayout] height:24
A change in animatedHandleHeight causes isLayoutCalculated and animatedSnapPoints to react to this change.
In the case of Strict Mode enabled, it looks like animatedHandleHeight doesn't get its new value from the BottomSheetHandleContainer but retains the INITIAL_HANDLE_HEIGHT of -999 which causes isHandleHeightCalculated to remain false and therefore isLayoutCalculated to be false.
Need to determine why Strict Mode causes this to happen.
In my previous comment, I said:
In the case of Strict Mode enabled, it looks like
animatedHandleHeightdoesn't get its new value from theBottomSheetHandleContainerbut retains theINITIAL_HANDLE_HEIGHTof-999which causesisHandleHeightCalculatedto remainfalseand thereforeisLayoutCalculatedto befalse.
I've found this is only partially true. What is true is animatedHandleHeight being the INITIAL_HANDLE_HEIGHT (-999) which causes isHandleHeightCalculated to remain false and therefore isLayoutCalculated to be false. However, it's not because the value doesn't change.
I added some more logs to reveal more of what is going on. Here are the logs when Strict Mode is disabled:
[handleMountSheet]
[handlePresent]
[Portal::handleOnMount]
[useReactiveSharedValue] name:animatedHandleHeight value:-999
[useAnimatedSnapPoints::normalizedSnapPoints] snapPoints:50% containerHeight:852 handleHeight:-999 contentHeight:-999 footerHeight:0 enableDynamicSizing:false maxDynamicContentSize:undefined dynamicSnapPointIndex:-1
[useAnimatedSnapPoints::hasDynamicSnapPoint] snapPoints:50% containerHeight:852 contentHeight:-999 handleHeight:-999 footerHeight:0 enableDynamicSizing:false maxDynamicContentSize:undefined
[BottomSheet::isLayoutCalculated] isContainerHeightCalculated:true isHandleHeightCalculated:false isSnapPointsNormalized:true
[BottomSheetView::handleLayout] height:426
[BottomSheetHandleContainer::handleContainerLayout] height:24
[useAnimatedSnapPoints::normalizedSnapPoints] snapPoints:50% containerHeight:852 handleHeight:-999 contentHeight:-999 footerHeight:0 enableDynamicSizing:false maxDynamicContentSize:undefined dynamicSnapPointIndex:-1
[useAnimatedSnapPoints::hasDynamicSnapPoint] snapPoints:50% containerHeight:852 contentHeight:-999 handleHeight:-999 footerHeight:0 enableDynamicSizing:false maxDynamicContentSize:undefined
[fun::isLayoutCalculated] isContainerHeightCalculated:true isHandleHeightCalculated:false isSnapPointsNormalized:true
[fun::useAnimatedReaction::MaybeSnapPointChange] result:426 previous:null isLayoutCalculated:false
[useAnimatedSnapPoints::normalizedSnapPoints] snapPoints:50% containerHeight:852 handleHeight:24 contentHeight:-999 footerHeight:0 enableDynamicSizing:false maxDynamicContentSize:undefined dynamicSnapPointIndex:-1
[useAnimatedSnapPoints::hasDynamicSnapPoint] snapPoints:50% containerHeight:852 contentHeight:-999 handleHeight:24 footerHeight:0 enableDynamicSizing:false maxDynamicContentSize:undefined
[fun::isLayoutCalculated] isContainerHeightCalculated:true isHandleHeightCalculated:true isSnapPointsNormalized:true
[fun::useAnimatedReaction::MaybeSnapPointChange] result:426 previous:426 isLayoutCalculated:true
[fun::useAnimatedReaction::OnSnapPointChange] result:426
[fun::bound evaluatePosition_Gorhom_BottomSheetTsx15] animationConfigs:undefined source:5
[fun::bound animateToPosition_Gorhom_BottomSheetTsx12] currentPosition:852 nextPosition:426 source:1
[BottomSheet::handleOnAnimate] toIndex:0 fromIndex:-1
[fun::bound animateToPositionCompleted_Gorhom_BottomSheetTsx11] animatedCurrentIndex:-1 animatedNextPosition:426 animatedNextPositionIndex:0
[fun::useAnimatedReaction::OnChange] animatedCurrentIndex:-1 animatedIndex:0
[BottomSheet::handleOnChange] index:0 animatedCurrentIndex:0
[handleBottomSheetOnChange] minimized:false forcedDismissed:false
The log starting with [BottomSheetHandleContainer::handleContainerLayout] is where animatedHandleHeight gets a value set. Note that when deriving the new values in useAnimatedSnapPoints, handleHeight is still -999. It turns out, there's another pass of deriving the new values in useAnimatedSnapPoints which does have the updated handleHeight. When Strict Mode is enabled the second pass does not occur (the logs stop at the first [fun::useAnimatedReaction::MaybeSnapPointChange]).
I don't yet fully understand why the first time the values are derived in useAnimatedSnapPoints after animatedHandleHeight is changed does not have the updated animatedHandleHeight and why it requires a second run to get the updated values; or why Strict Mode is affecting this. It may have something to do with how React works, React Native Reanimated doing different pieces of work on different threads and the JS event loop.
It may be an issue that's the responsibility of React Native Reanimated. I'm trying to create an MCVE that's only uses React Native Reanimated but haven't successfully done so and am unsure if I will because I've found a workaround which means I'll be de-prioritising further investigation.
The workaround I've found is patching BottomSheetHandleContainer to set the animatedHandleHeight value in a new macro-task by using setTimeout(..., 0):
const handleContainerLayout = useCallback(
function handleContainerLayout({
nativeEvent: {
layout: { height },
},
}: LayoutChangeEvent) {
- handleHeight.value = height;
+ setTimeout(() => {
+ handleHeight.value = height;
+ }, 0);
if (__DEV__) {
print({
component: BottomSheetHandleContainer.displayName,
method: 'handleContainerLayout',
category: 'layout',
params: {
height,
},
});
}
},
[handleHeight]
);
Will drop more comments if I continue investigating and find something new. Hope the comments so far have been useful.
Hmm, the setTimeout workaround doesn't seem to be foolproof. If enableDynamicSizing is true it doesn't always work (~10% of the time) and a different issue where the sheet takes the height of the screen happens (~30% of the time). Also, attempting to ignore the deprecated API error logs using LogBox.ignoreLogs when enableDynamicSizing is true stops the workaround from working at all π΅βπ«
thanks for diving into this, i'll look into it shortly
Thanks for such a thorough investigation, following as I'm also seeing this same error.
@gorhom: I'm not sure if the OR condition is correct here: https://github.com/gorhom/react-native-bottom-sheet/blob/2898ada5863f05fb26daadcb04068d1a7432dee9/src/components/bottomSheet/BottomSheet.tsx#L258-L263
When _providedContainerHeight is:
null, the condition gets evaluated like this:null !== null || null !== undefinedfalse || truetrue
undefined, the condition gets evaluated like this:undefined !== null || undefined !== undefinedtrue || falsetrue
It doesn't solve the issue reported but I spotted it while looking at this issue so flagging here.
@gorhom I'm seeing the same error in the following environment, could you kindly look into this? π
"expo": "~52.0.42",
"react": "18.3.1",
"react-native": "0.76.8",
"react-native-gesture-handler": "~2.20.2",
"react-native-reanimated": "~3.16.7",
"@gorhom/bottom-sheet": "^5.1.2",
currently getting the same issue
Also encountered this problem using react-nativeΒ 0.79.5 and the latest version of this lib