react-native-bottom-sheet: Modal reopens while dismissing if it gets re-rendered while closing

Bug

This is a bit more precise version (with reproducible code sample) of #191.

If your handler call dismiss but also triggers the modal to get updated, then modal doesn’t get dismissed, it reopens.

https://user-images.githubusercontent.com/6768840/104815091-e1482c00-5855-11eb-9797-048efd23f69e.mov

Full repro is noted below (thought it only contains 1 line diff from the example app), and related screen code is as below.

import React, { useCallback, useMemo, useRef, useState } from 'react';
import { View, StyleSheet, Text } from 'react-native';
import { BottomSheetModal, BottomSheetView } from '@gorhom/bottom-sheet';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import Button from '../../components/button';
import withModalProvider from '../withModalProvider';

const DynamicSnapPointExample = () => {
  // state
  const [count, setCount] = useState(0);
  const [contentHeight, setContentHeight] = useState(0);

  // hooks
  const bottomSheetRef = useRef<BottomSheetModal>(null);
  const { bottom: safeBottomArea } = useSafeAreaInsets();

  // variables
  const snapPoints = useMemo(() => [contentHeight], [contentHeight]);

  // callbacks
  const handleIncreaseContentPress = useCallback(() => {
    setCount(state => state + 1);
  }, []);
  const handleDecreaseContentPress = useCallback(() => {
    setCount(state => Math.max(state - 1, 0));
  }, []);

  const handlePresentPress = useCallback(() => {
    bottomSheetRef.current?.present();
  }, []);
  const handleDismissPress = useCallback(() => {
    // NOTE: This setCount call triggers the issue
    setCount(state => state + 1);
    bottomSheetRef.current?.dismiss();
  }, []);
  const handleOnLayout = useCallback(
    ({
      nativeEvent: {
        layout: { height },
      },
    }) => {
      setContentHeight(height);
    },
    []
  );

  // styles
  const contentContainerStyle = useMemo(
    () => ({
      ...styles.contentContainerStyle,
      paddingBottom: safeBottomArea,
    }),
    [safeBottomArea]
  );
  const emojiContainerStyle = useMemo(
    () => ({
      ...styles.emojiContainer,
      height: 50 * count,
    }),
    [count]
  );

  // renders
  const renderBackground = useCallback(
    () => <View style={styles.background} />,
    []
  );

  return (
    <View style={styles.container}>
      <Button
        label="Present"
        style={styles.buttonContainer}
        onPress={handlePresentPress}
      />
      <Button
        label="Dismiss"
        style={styles.buttonContainer}
        onPress={handleDismissPress}
      />
      <BottomSheetModal
        ref={bottomSheetRef}
        index={0}
        snapPoints={snapPoints}
        backgroundComponent={renderBackground}
      >
        <BottomSheetView
          style={contentContainerStyle}
          onLayout={handleOnLayout}
        >
          <Text style={styles.message}>
            Could this sheet modal resize to its content height ?
          </Text>
          <View style={emojiContainerStyle}>
            <Text style={styles.emoji}>😍</Text>
          </View>
          <Button
            label="Yes"
            style={styles.buttonContainer}
            onPress={handleIncreaseContentPress}
          />
          <Button
            label="Maybe"
            style={styles.buttonContainer}
            onPress={handleDecreaseContentPress}
          />
        </BottomSheetView>
      </BottomSheetModal>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 24,
  },
  background: {
    ...StyleSheet.absoluteFillObject,
    backgroundColor: 'white',
  },
  buttonContainer: {
    marginBottom: 6,
  },
  contentContainerStyle: {
    paddingTop: 12,
    paddingHorizontal: 24,
    backgroundColor: 'white',
  },
  message: {
    fontSize: 24,
    fontWeight: '600',
    marginBottom: 12,
  },
  emoji: {
    fontSize: 156,
    textAlign: 'center',
    alignSelf: 'center',
  },
  emojiContainer: {
    overflow: 'hidden',
    justifyContent: 'center',
  },
});

export default withModalProvider(DynamicSnapPointExample);

Environment info

Library Version
@gorhom/bottom-sheet 2.0.4
react-native 0.63.4
react-native-reanimated 1.13.2
react-native-gesture-handler 1.9.0

Steps To Reproduce

  1. Clone the repo below
  2. Run the example app and select MODAL > Dynamic Snap Point example
  3. Press Present
  4. Press Dismiss

Describe what you expected to happen:

  1. Modal closes without reopening

Reproducible sample code

https://github.com/heejongahn/react-native-bottom-sheet

You can verify this behavior with example/src/sceens/modal/DynamicSnapPointExample.tsx. (diff from original example)

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 1
  • Comments: 22 (5 by maintainers)

Most upvoted comments

I’ve just read through all the discussions on this issue, do we have an update? I’m still experiencing this problem… 😦

As always… I found the issue not long after posting this comment… I really did not think this would fix my issue, but it did.

From https://github.com/gorhom/react-native-bottom-sheet/pull/205#issuecomment-763074453

Wrapping my easing function in a memo worked:

From:

const animationConfigs = useBottomSheetTimingConfigs({
    duration: 400,
    easing: Easing.inOut(Easing.cubic)
})

To:

const easingConfig = useMemo(() => Easing.inOut(Easing.cubic), [])

const animationConfigs = useBottomSheetTimingConfigs({
    duration: 400,
    easing: easingConfig
})

Same problem here using tab bar, when I navigate from the tab that have the bottom sheet (and its closed) and then comeback to the same tab, the bottom sheet appears open. The problem is only with Android.

FYI This issue still exists in v2.3.0.

Is it really fixed for everyone? Happening to me on v2.3.0.

ref?.current?.dismiss(); button.onPress() Where onPress leads to rerender of BottomSheet children, causes modal to get stuck in some minimized but not unmounted state.

ref?.current?.dismiss(); setTimeout(() => {button.onPress()}, 1000) On the other hand works perfectly, because rerender definetly occurs after modal was dismissed.

Too bad, that dismiss() doesn’t return a promise, then we could call critical methods, when promise was resolved.

this should be fixed with v2.0.7 🎉

For those interested and or still having issues, I found a fix for this.

The example provided in the documentation has the index prop equal to 1, so in the case the bottom sheet is unmounted, then it will default back to 1 (even if you specify snapToIndex or snapToPosition on mount using useEffect, which it will ignore to prioritize the default index).

To fix this we need to have the index prop equal to the last index value before it was unmounted, but using state hooks will result in a re-render every index change. This is what you do instead:

const bottomSheetIndex = useRef({ index: -1 })

return (
  <BottomSheet
    index={bottomSheetIndex.current.index}
    onChange{(index) => { bottomSheetIndex.current.index = index }}
  >
  {Your stuff}
  </BottomSheet>
)

Now the index is tracked without re-rendering each update and if the bottom sheet is unmounted, it will reference the last known index.

Hope this helps! 😊

- Logan (a.k.a. DarkComet)

Massive thanks to you!

Something similar happens in 3.2.1 but when opening the bottom sheet immediately before or after setting any state that would trigger a re-render of the content inside the bottom sheet causes it to close back down mid-animation. A workaround I had to do was doing setTimeout(() => bottomSheetRef.current.snapTo(1), 200) which isn’t ideal

thanks @heejongahn for submitting this issue, i will look into it shortly