question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Bottom sheet modal doesn't open in production

See original GitHub issue

Bug

Thanks for taking the time to look at this.

In production it seems that the bottom modal sheet flicks and doesn’t open. If you look at my video attached you will see that the modal flickers slightly down the bottom sometimes it opens fully for a brief half second then closes. I’ve checked if the dismiss call is firing on open which it’s not.

Any help would be greatly appreciated.

UPDATE: I’m now calling present() in a useFocusHook and the modal is still flickering down the bottom. It appears half way up the screen for half a second then flickers and goes away.

Image from iOS

Environment info

iOS - Test Flight

Library Version
@gorhom/bottom-sheet ^1.4.1
react-native 0.62.2
react-native-reanimated ^1.9.0
react-native-gesture-handler ^1.6.1

Steps To Reproduce

1.Press on button to trigger present() Describe what you expected to happen: 1.Modal to open

Reproducible sample code

  • BottomSheet component - Called in screen
const BottomSheetHomeScreenComponent = ({myRef, listData, renderList, extraData, onSearchButtonPress, searchInput, userReducer, isLoading, navigation}) => {
  const snapPoints = useMemo(() => ['75%', '90%'], []);

  // This controls what is being displayed whether the user is logged in / if they have created a story
  const noData = ({section}) => {
    if (section.data.length === 0) {
      return (
        <View style={{alignSelf: 'center', marginTop: 30}}>
          <Text>No stories found for this search</Text>
        </View>
      )
    }
  };

  // This handles the header and what is being displayed depending if the user is logged in or not. 
  const handleHeaderDisplayComponent = () => {
    return (
      <ButtonComponent 
        title={searchInput === "" ? "Press for more search options" : searchInput}
        containerStyle={styles.searchButtonContainerStyle}
        buttonStyle={styles.searchButtonStyle}
        titleStyle={styles.searchButtonTitleStyle}
        onButtonPress={() => navigation.push(SEARCH_SCREEN)}
        icon={
          <IconComponent
            name="search"
            type="font-awesome-5"
            size={ICON_SIZE.iconSizeSmall}
            color={COLOR.limeGreen}
            style={{marginRight: 20}}
          />
        }
      /> 
    )
  };

  const handleSectionHeader = (section) => {
    return (
      !section.data.length ? (
        null
      ) : (
        <View style={styles.sectionTitle}>
          <Text style = {styles.headerText}>{section.title}</Text>
        </View>
      )
    )
  };

  const handleBackgroundComponent = () => {
    return <FeatureComponent userReducer={userReducer} allCollectionsReducer={extraData} />
  };

  return (
    <BottomSheet 
      ref={myRef}
      initialSnapIndex={0}
      snapPoints={snapPoints}
      shouldMeasureContentHeight={true}
      backgroundComponent={handleBackgroundComponent}
    >
      <View style={styles.contentContainer}>
        {listData[0].data === null || isLoading ? (
          <View style={{marginLeft: 10, marginRight: 10}}>
            {handleHeaderDisplayComponent()}
            <View style={{marginTop: 10}}>
              <SkeletonStoryCard />
            </View>
          </View>
          ) : (
          <BottomSheetSectionList 
            sections={listData}
            renderItem={renderList}
            renderSectionHeader={({ section }) => handleSectionHeader(section)}
            keyExtractor={(item) => item.id}
            extraData={listData.data}
            keyboardShouldPersistTaps='always'
            stickySectionHeadersEnabled={false}
            ListHeaderComponent={handleHeaderDisplayComponent}
            renderSectionFooter={noData}
          />
        )}
      </View>
    </BottomSheet>
  )
};
  • Screen - Parent
const SearchScreen = ({ navigation, userReducer, allCollectionsReducer, addBookMarkedStory, removeBookMarkedStory }) => {

  const alertContext = useAlertContext();
  const { present, dismiss } = useBottomSheetModal();

  // State
  const [search, setSearch] = useState("");
  const [recentSearchHistory, setRecentSearchHistory] = useState([]);
  const [storyResults, setStoryResults] = useState([]);
  const [isLoading, setIsLoading] = useState(false);

  // bottomSheet
  const contentSheetRef = null;

  // Makes the search full page modal appear 
  const handlePresentPress = useCallback(() => {
    present(
        <SearchModalComponent 
          updateSearch={(search) => updateSearch(search)} 
          onTagSelect={(tag, id) => (updateSearch(tag.title, id), handleDismissPress())}
          onSubmit={(search) => (saveRecentSearchToStorage(search), updateSearch(search), handleDismissPress())}
          onRecentSearchSelect={(recentSearch) => (updateSearch(recentSearch), handleDismissPress())}
          search={search} 
          recentSearchHistory={recentSearchHistory}
          handleOnDismiss={() => handleDismissPress()}
        />,
      {
        snapPoints: ['95%', '95%'], 
        dismissOnOverlayPress: false,
        dismissOnScrollDown: false
      },
    );
  }, [present, search, recentSearchHistory]);

  const handleDismissPress = useCallback(() => {
    Alert.alert('IT HAS BEEN CALLED')
    dismiss();
  }, [dismiss]);

  const updateSearch = async (search, id) => {
    setIsLoading(true);
    
    // Sets the search to update
    setSearch(search);

    // Calls API
    const response = await getSearchResults(search, id);

    if (response.status === 200) {
      // If no data returned still ensure that the states are empty arrays
      setStoryResults(!response.data ? [] : response.data.stories);
      return setIsLoading(false);
    } else {
      console.log('error searching')
      return setIsLoading(false);
    }
  };

  // When the user is viewing the screen for the first time this will fire.
  const userTutorial = async () => {
    const screenTutorial = await getToken(SEARCH_SCREEN_TUTORIAL);
    if (screenTutorial === null) {
      storeToken(SEARCH_SCREEN_TUTORIAL, 'complete');
      // If user is not logged in and users first open of app display this pop-up
      return alertContext.alert({
        title: "Welcome to the search page!",
        body: "You can search for users and stories here",
        display: 'modal',
        theme: 'modalWelcomeTheme',
        iconName: 'search',
        iconColor: COLOR.limeGreen,
        iconSize: ICON_SIZE.iconSizeXXLarge,
      });
    } else {
      return;
    }
  };

  // Save search to local storage 
  const saveRecentSearchToStorage = async (search) => {
    if (recentSearchHistory.length === 3) {
      recentSearchHistory.unshift({text: search});
      recentSearchHistory.pop();
    } else { 
      recentSearchHistory.unshift({text: search});
    }
    
    await storeToken(RECENT_SEARCH_HISTORY_IDENTIFIER, JSON.stringify(recentSearchHistory))
  };

  // Recovers recent search history from local storage
  const recoverRecentSearchHistory = async () => {
    const data = await getToken(RECENT_SEARCH_HISTORY_IDENTIFIER);
    setRecentSearchHistory(data ? JSON.parse(data) : []);
  };

  useFocusEffect(
    useCallback(() => {
      recoverRecentSearchHistory();
      userTutorial();
    }, []),
  );
  
  // Only sets the search with all stories on first load. If user searches and clears search it will return all stories
  useEffect(() => {
    setStoryResults(allCollectionsReducer.stories);
  }, [allCollectionsReducer.stories])

  // When the user clicks on story card
  const onStoryPress = (storyID) => {
    const userID = userReducer.id

    // Navigates to the Screens navigator then storyStack then to view Story
    return navigation.navigate(SCREENS_NAVIGATOR, {
      screen: STORY_SCREEN_STACK,
      params: {
        screen: VIEW_STORY_SCREEN,
        params: { storyID, userID }
      },
    });
  };

  // Handle when user clicks on bookmark button
  const onBookmarkPress = async (hasUserBookMarkedStory, storyID) => {

    // If user is not logged in
    if (!userReducer.id) {
      return navigation.navigate(AUTH_NAVIGATOR, {
        screen: AUTH_SCREEN_STACK,
        params: {
          screen: LOGIN_SCREEN
        }
      });
    };

    // If already bookmarked 
    if (hasUserBookMarkedStory) {
      const response = await unBookMarkStory(storyID, userReducer.id);
      response.status === 200 ? removeBookMarkedStory(storyID) : console.log("ERROR");
    } else {
      const response = await bookMarkStory(storyID, userReducer.id);
      response.status === 200 ? addBookMarkedStory(storyID) : console.log("ERROR");
    };
  };

  // Renders story cards
  const renderResults = ({item}) => {
    const hasUserLikedStory = userReducer.likedStories ? userReducer.likedStories.includes(item.id) : false;
    const hasUserBookMarkedStory = userReducer.bookMarks ? userReducer.bookMarks.includes(item.id) : false;
  
    return (
      <TouchableOpacity onPress={() => onStoryPress(item.id)} style={styles.card}>
        <StoryCardComponent
          title={item.title}
          description={item.description}
          tags={item.tags}
          avatarURL={item.interviewer.avatarURL}
          likes={item.likes}
          hasUserLikedStory={hasUserLikedStory}
          hasUserBookMarkedStory={hasUserBookMarkedStory}
          onBookMarkPress={() => onBookmarkPress(hasUserBookMarkedStory, item.id)}
        />
      </TouchableOpacity>
    );
  };

  return (
    <View style={styles.container}>
      <BottomSheetSearchComponent 
        onSearchButtonPress={() => handlePresentPress()}
        myRef={contentSheetRef}
        listData={[
          {title: 'Stories', data: storyResults}
        ]}
        renderList={renderResults}
        extraData={allCollectionsReducer}
        userReducer={userReducer}
        searchInput={search}
        isLoading={isLoading}
      />
    </View>
  );
};
  • Modal
const SearchModalComponent = ({recentSearchHistory: _recentSearchHistory, search: _search, updateSearch, onSubmit, onTagSelect, handleOnDismiss, onRecentSearchSelect, storyReducer}) => {

  const [search, setSearch] = useState(_search);
  const [recentSearchHistory, setRecentSearchHistory] = useState(_recentSearchHistory);
  const mockArray = [];

  const handleInputChange = useCallback((search) => {
    setSearch(search);
    return updateSearch(search);
  }, [updateSearch]);

  // This displays the tags in the search modal
  const displayTags = () => {
    const tag = storyReducer.allTags.map((tag) => {
      return (
        <TagComponent 
          key={tag.id}
          tag={tag}
          onSelectedTag={(tag) => onTagSelect(tag, tag.id)}
        />
      )
    });

    return tag;
  };

  const mockLoadingTags = () => {
    for (let i = 0; i < 10; i++) {
      mockArray.push(i);
    };
  };

  const displayRecentSearch = () => {
    const recentSearch = recentSearchHistory.map((recent, index) => {
      return (
        <View style={styles.recentSearchComponentContainer} key={index}>
          <RecentSearchComponent 
            title={recent.text}
            onSelect={(title) => onRecentSearchSelect(title)}
          />
        </View>
      )
    });

    return recentSearch;
  };

  return (
    <View style={styles.container}>
      <ScrollView style={styles.contentContainer} keyboardShouldPersistTaps={'handled'} showsVerticalScrollIndicator={false}>
        
        <TouchableOpacity onPress={() => handleOnDismiss()} style={styles.masterHeaderContainer}>
          <IconComponent
            name="chevron-left"
            type="font-awesome-5"
            size={ICON_SIZE.iconSizeSmall}
            color={COLOR.grey}
            style={styles.iconStyle}
          />
          <Text style={styles.masterHeader}>Search</Text>
        </TouchableOpacity>
        

        <TextInputComponent 
          onChange={(search) => handleInputChange(search)}
          onSubmitEditing={(search) => onSubmit(search)}
          containerStyle={styles.searchBarContainerStyle}
          iconName="search"
          iconType="font-awesome"
          inputContainerStyle={styles.inputContainerStyle}
          placeholder="Search for stories here"
          returnKeyType="search"
          clearTextOnFocus={true}
          enablesReturnKeyAutomatically={true}
        />
        <Text style={styles.header}>Search by recent</Text>
        <View style={styles.recentSearchContainer}>
          {displayRecentSearch()}
        </View>

        <Text style={styles.header}>Search by tags</Text>
        <View style={styles.tagContainer}>
          {storyReducer.allTags === null ? (
            mockLoadingTags(),
            mockArray.map((el, i) => {
              return <SkeletonTagComponent key={i} />
            })
          ) : (
            displayTags()
          )}
        </View>
      </ScrollView>
    </View>
  )
};

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Comments:5 (2 by maintainers)

github_iconTop GitHub Comments

3reactions
MarcusOycommented, Dec 11, 2020

Hey @gorhom , I’m not the OP, but since I’ve been experiencing this same issue, I’ve created a stripped down version of my app for you to take a look at here. I know its not something that runs from /example, which works fine on my end, but maybe there is a problem relating with Expo.

Quick instructions: yarn install and expo start, then press either a to launch Android or i to launch iOS.

Here’s two GIFs of what I’m seeing from both platforms:

Android: Working as intended. BottomSheetAndroid

iOS: Not working as intended. BottomSheetiOS

Edit: Let me know if this is a different bug, since the setup is a bit different, so that I can open up a new issue.

2reactions
gorhomcommented, Dec 13, 2020

hi @maxckelly, @MarcusOy I just released Alpha 5, it should fix this issue, please try it and let me know , thanks

yarn add @gorhom/bottom-sheet@2.0.0-alpha.5

also check out the new documents website ( still in progress ) 🎉

Read more comments on GitHub >

github_iconTop Results From Across the Web

Android Bottom Sheet Modal (Dialog) doesn't open completely
I am trying to show a bottom sheet dialog in my app on a button click. But the dialog is opening partially. I...
Read more >
How to Create Modal Bottom Sheet in Android | Java - Medium
In this blog post, we're going to create a modal bottom sheet in android. Nowadays most app using this material design concept in...
Read more >
Modal & Nonmodal Dialogs: When (& When Not) to Use Them
Modal dialogs interrupt users and demand an action. They are appropriate when user's attention needs to be directed toward important ...
Read more >
Most performant modal/bottom sheet library? : r/reactnative
The modal provided by react native themselves doesn't have a backdrop or slide down feature, so that doesn't work. The rest of the...
Read more >
Newest 'bottom-sheet' Questions - Stack Overflow
I got an error No default value Have used Bottom Sheet library and used it's components as shown below: BottomSheetModalProvider BottomSheet ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found