How To Use BottomSheet With React Navigation

How To Use BottomSheet With React Navigation

Β·

4 min read

Hashnode app is coming up with some exciting updates which deliver a fully native article reading experience. The designs included multiple occasions of a bottom or action sheet for different purposes. One was to show more options for an article or comment, the other was to show the reactions added by the current user.

I really love when code is reusable and decoupled throughout an application. Where possible, I try to write self-contained and managed components, that more or less react to their environment rather than being controlled by a parent. This is especially the case, when there are no unique actions in the component, so that all side effects or passed callbacks are duplicates throughout the use of the component.

At this point, we got an Idea for the more options and reactions components:

Why not utilize our navigation solution with the bottom sheet to get a self-contained component?

So, we implemented the bottom sheet within a transparent modal view and:

Working!

What was used?

Actually, we only used these two libraries, but keep in mind, they have some depencendies that need to be installed as well:

Why bother?

So, why bother to let the BottomSheet be handled in the navigation lib?

Let's take a look at this simple sheet:

import React, { useCallback, useMemo, useRef } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import BottomSheet from '@gorhom/bottom-sheet';

const MyBottomSheet = () => {
  // ref
  const bottomSheetRef = useRef<BottomSheet>(null);

  // variables
  const snapPoints = useMemo(() => ['25%', '50%'], []);

  // callbacks
  const handleSheetChanges = useCallback((index: number) => {
    console.log('handleSheetChanges', index);
  }, []);

  // renders
  return (
    <View style={styles.container}>
      <BottomSheet
        ref={bottomSheetRef}
        index={1}
        snapPoints={snapPoints}
        onChange={handleSheetChanges}
      >
        <View style={styles.contentContainer}>
          <Text>Awesome πŸŽ‰</Text>
        </View>
      </BottomSheet>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 24,
    backgroundColor: 'grey',
  },
  contentContainer: {
    flex: 1,
    alignItems: 'center',
  },
});

export default MyBottomSheet;

We can see, that a sheet takes index which will indicate the initial! snap point, therefore it controls the display itself and provides a callback onChange for listening to sheet position changes.

Initially, I don't want to have the sheet displayed when rendering any component, using the above sheet like this, would display it immediately.

const CommentsScreen = (props) => {
...
  return (
    <Container>
      <CommentList />
      <CommentEditor />
      <MyBottomSheet />
    </Container>
  );
};

A solution could be, to set the initial index to -1 which hides the sheet, and pass down a ref toMyBottomSheet to invoke the snapToIndex or expand function of the sheet. Another solution would be to add the sheet code directly to the parent component and access the ref directly. Both solutions are feasible but would introduce some overhead in managing the sheet within its parent. At least, in our case. There are use-cases where we want to have this control. Just think about the usage of a BottomSheet in a maps screen.

Use it with React-Navigation

The solution we took for the BottomSheet was to add it in its own route and keep its management completely autonomous.

This could look something like that:

     <Stack.Screen
        name={AppArticleRoutes.MORE_OPTIONS}
        component={MyBottomSheet}
        options={{
          headerShown: false,
          presentation: 'transparentModal',
          animation: 'none',
        }}
      />

We disable the animation for screen mount as the BottomSheet has its own animation when mounted. Also, to get the transparent background so that the previous screen is still visible, we set the presentation to transparentModal. Keep in mind, that this needs to be rendered within a "native-stack" from React-Navigation.

In our Screens that want to display the BottomSheet we can then add this call to open the sheet:

     navigation.navigate(AppArticleRoutes.MORE_OPTIONS, {
       articleId,
        variant: 'article',
      });

Just providing the intended article identifier as well as the variant which controls the display, we can let react-navigation take care of mounting the BottomSheet.

Within the sheet we can utilize the onChange callback to listen to close events and navigate back:

    const handleSheetChanges = useCallback(
      (index: number) => {
        if (index === -1) {
          navigation.goBack();
        }
      },
      [navigation],
    );

Benefits

What benefits are there to using the BottomSheet with React-Navigation?

  • The sheet is not mounted until needed
  • We don't need to manage the visibility within the parent component
  • We can reuse this BottomSheet in multiple places without needing to add it to the respective components
  • Reducing duplicated code

To sum up, we have gained some reusable code that is completely managed within the navigation lib by using the little trick to display a BottomSheet in a transparent modal and disabling animations on screen mount.

If you folks are interested in more articles like that and how we are building the Hashnode App take a look at our new mobile series, where we are going to share more stories like this.

A big thanks here to the authors of the libraries Gorhom's, React-navigation for making React Native app development a lot easier with your great work πŸš€

Until next time folks,

See ya! πŸ‘‹πŸ»