Create a react-native app with a nested multi-level drawer menu.
In this article, we'll use react-navigation to manage the drawer menu.
The code of the whole app build here is available at https://github.com/Merlier/rn_example_menu_drawer_nested.git
Note: The app.js show here is simplified. To code cleanly you have to export all your navigation and screen functions in separated jsx files.
Get started
Requirements:
- react-native >= 0.60
First just init a new react-native project:
$ npx react-native init rn_example_menu_drawer_nested
Install the react-navigation module:
$ npm install --save @react-navigation/native
$ npm install --save react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-view
Add the thwo screens Home and Settings by modifying the app.js:
/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* @format
* @flow strict-local
*/
import 'react-native-gesture-handler';
import React from 'react';
import {StyleSheet, View, Text} from 'react-native';
import {NavigationContainer} from '@react-navigation/native';
import {createStackNavigator} from '@react-navigation/stack';
function HomeScreen() {
return (
<View style={{flex: 1, alignItems: 'center', justifyContent: 'center'}}>
<Text>Home Screen</Text>
</View>
);
}
function SettingsScreen() {
return (
<View style={{flex: 1, alignItems: 'center', justifyContent: 'center'}}>
<Text>Settings Screen</Text>
</View>
);
}
const Stack = createStackNavigator();
const App: () => React$Node = () => {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Settings" component={SettingsScreen} />
</Stack.Navigator>
</NavigationContainer>
);
};
const styles = StyleSheet.create({});
export default App;
Just a simple drawer
We will now build a simple drawer menu.
First install the corresponding drawer module:
$ npm install --save @react-navigation/drawer
Add the drawerItemsMain.js which will contain all the drawer route definition:
export const drawerItemsMain = [
{
key: 'Home',
title: 'Home',
route: {nav: 'MainDrawer', routeName: 'Home', title: 'Home'},
},
{
key: 'Settings',
title: 'Settings',
route: {nav: 'MainDrawer', routeName: 'Settings', title: 'Settings'},
},
];
Create the CustomDrawerContent.js to make the rendering of our custom drawer:
import React from 'react';
import {
StyleSheet,
ScrollView,
View,
Text,
TouchableOpacity,
SafeAreaView,
Image,
} from 'react-native';
function CustomDrawerContent(props) {
const onItemPress = (key) => {
const filteredMainDrawerRoutes = props.drawerItems.find((e) => {
return e.key === key;
});
const selectedRoute = filteredMainDrawerRoutes.route;
props.navigation.toggleDrawer();
props.navigation.navigate(selectedRoute.nav, {
screen: selectedRoute.routeName,
});
};
const logOut = async () => console.log('log out');
function renderMainDrawer() {
return (
<View>
{props.drawerItems.map((parent) => (
<View key={parent.key}>
<TouchableOpacity
key={parent.key}
testID={parent.key}
onPress={() => {
onItemPress(parent.key);
}}>
<View style={styles.parentItem}>
<Text style={[styles.icon, styles.title]}>{parent.title}</Text>
</View>
</TouchableOpacity>
</View>
))}
{renderLogoutBtn()}
</View>
);
}
function renderLogoutBtn() {
return (
<View>
<TouchableOpacity onPress={logOut} testID="customDrawer-logout">
<View style={styles.parentItem}>
<Text style={styles.title}>{'Log out'}</Text>
</View>
</TouchableOpacity>
</View>
);
}
return (
<ScrollView style={styles.drawerContainer}>
<SafeAreaView
style={styles.container}
forceInset={{top: 'always', horizontal: 'never'}}>
<View style={styles.centered}>
<Image
source={{uri: 'https://reactjs.org/logo-og.png'}}
style={styles.logo}
/>
</View>
{renderMainDrawer()}
</SafeAreaView>
</ScrollView>
);
}
const styles = StyleSheet.create({
headerContainer: {
height: 100,
flexDirection: 'row',
paddingVertical: 28,
justifyContent: 'center',
alignItems: 'center',
},
logo: {
width: 100,
height: 75,
},
drawerContainer: {
backgroundColor: '#222222',
},
container: {
flex: 1,
zIndex: 1000,
},
centered: {
alignItems: 'center',
},
parentItem: {
flexDirection: 'row',
alignItems: 'center',
borderBottomWidth: 1,
borderBottomColor: '#F0F0F0',
paddingTop: 4,
paddingBottom: 4,
},
title: {
margin: 16,
fontWeight: 'bold',
color: '#F0F0F0',
textAlign: 'center',
},
});
export default CustomDrawerContent;
Then just add a CustomHeader.js to make your custom header:
import React from 'react';
import {
StyleSheet,
TouchableOpacity,
View,
Text,
SafeAreaView,
} from 'react-native';
import {DrawerActions} from '@react-navigation/native';
function CustomHeader(props) {
const toggleDrawer = () =>
props.navigation.dispatch(DrawerActions.toggleDrawer());
return (
<SafeAreaView>
<View style={styles.headerContainer}>
<View style={styles.headerLeft}>
<TouchableOpacity
onPress={toggleDrawer}
style={styles.leftButton}
testID="CustomHeader-toggleDrawer">
<Text style={styles.buttonTxt}>MENU</Text>
</TouchableOpacity>
</View>
<View style={styles.header}>
<Text style={styles.headerTxt}>HEADER</Text>
</View>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
headerContainer: {
flexDirection: 'row',
justifyContent: 'space-around',
alignItems: 'center',
backgroundColor: '#222222',
minHeight: 40,
},
headerLeft: {
flexDirection: 'row',
},
leftButton: {
marginLeft: 10,
},
header: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
paddingRight: 40,
},
buttonTxt: {
color: '#ddd',
fontWeight: 'bold',
},
headerTxt: {
color: '#ddd',
},
});
export default CustomHeader;
Modify the app.js like this:
...
import {createDrawerNavigator} from '@react-navigation/drawer';
import {drawerItemsMain} from './drawerItemsMain';
import CustomDrawerContent from './CustomDrawerContent.js';
import CustomHeader from './CustomHeader';
const Drawer = createDrawerNavigator();
...
function MainDrawerNavigation() {
return (
<Drawer.Navigator
initialRouteName="Home"
drawerContent={(props) => (
<CustomDrawerContent drawerItems={drawerItemsMain} {...props} />
)}>
<Drawer.Screen name="Home" component={HomeScreen} />
<Drawer.Screen name="Settings" component={SettingsScreen} />
</Drawer.Navigator>
);
}
const App: () => React$Node = () => {
return (
<NavigationContainer>
<Stack.Navigator
screenOptions={{
headerMode: 'screen',
headerTintColor: '#404554',
headerTitleStyle: {
fontWeight: 'bold',
},
header: (props) => {
return <CustomHeader {...props} />;
},
}}>
<Stack.Screen name="MainDrawer" component={MainDrawerNavigation} />
</Stack.Navigator>
</NavigationContainer>
);
};
...
Finally a nested multi-level drawer
Modify the draweritemsMain.js like this:
export const drawerItemsMain = [
{
key: 'Home',
title: 'Home',
routes: [{nav: 'MainDrawer', routeName: 'Home', title: 'Home'}],
},
{
key: 'Settings',
title: 'Settings',
routes: [
{nav: 'MainDrawer', routeName: 'Settings1', title: 'Settings 1'},
{nav: 'MainDrawer', routeName: 'Settings2', title: 'Settings 2'},
],
},
];
the CustomDrawerContent.js like this:
import React, {useState, useRef, useEffect} from 'react';
import {
StyleSheet,
ScrollView,
View,
Text,
TouchableOpacity,
SafeAreaView,
Image,
} from 'react-native';
function CustomDrawerContent(props) {
const [mainDrawer, setMainDrawer] = useState(true);
const [filteredItems, setFilteredItems] = useState([]);
const toggleMainDrawer = () => {
setMainDrawer(true);
setFilteredItems([]);
};
const onItemParentPress = (key) => {
const filteredMainDrawerRoutes = props.drawerItems.find((e) => {
return e.key === key;
});
if (filteredMainDrawerRoutes.routes.length === 1) {
const selectedRoute = filteredMainDrawerRoutes.routes[0];
props.navigation.toggleDrawer();
props.navigation.navigate(selectedRoute.nav, {
screen: selectedRoute.routeName,
});
} else {
setMainDrawer(false);
setFilteredItems(filteredMainDrawerRoutes);
}
};
const logOut = async () => console.log('log out');
function renderMainDrawer() {
return (
<View>
{props.drawerItems.map((parent) => (
<View key={parent.key}>
<TouchableOpacity
key={parent.key}
testID={parent.key}
onPress={() => {
onItemParentPress(parent.key);
}}>
<View style={styles.parentItem}>
<Text style={[styles.icon, styles.title]}>{parent.title}</Text>
</View>
</TouchableOpacity>
</View>
))}
{renderLogoutBtn()}
</View>
);
}
function renderFilteredItemsDrawer() {
return (
<View>
<TouchableOpacity
onPress={() => toggleMainDrawer()}
style={styles.backButtonRow}>
<Text style={[styles.backButtonText, styles.title]}>{'BACK'}</Text>
</TouchableOpacity>
{filteredItems.routes.map((route) => {
return (
<TouchableOpacity
key={route.routeName}
testID={route.routeName}
onPress={() =>
props.navigation.navigate(route.nav, {
screen: route.routeName,
})
}
style={styles.item}>
<Text style={styles.title}>{route.title}</Text>
</TouchableOpacity>
);
})}
</View>
);
}
function renderLogoutBtn() {
return (
<View>
<TouchableOpacity onPress={logOut} testID="customDrawer-logout">
<View style={styles.parentItem}>
<Text style={styles.title}>{'Log out'}</Text>
</View>
</TouchableOpacity>
</View>
);
}
return (
<ScrollView style={styles.drawerContainer}>
<SafeAreaView
style={styles.container}
forceInset={{top: 'always', horizontal: 'never'}}>
<View style={styles.centered}>
<Image
source={{uri: 'https://reactjs.org/logo-og.png'}}
style={styles.logo}
/>
</View>
{mainDrawer ? renderMainDrawer() : renderFilteredItemsDrawer()}
</SafeAreaView>
</ScrollView>
);
}
const styles = StyleSheet.create({
headerContainer: {
height: 100,
flexDirection: 'row',
paddingVertical: 28,
justifyContent: 'center',
alignItems: 'center',
},
logo: {
width: 100,
height: 75,
},
drawerContainer: {
backgroundColor: '#222222',
},
container: {
flex: 1,
zIndex: 1000,
},
centered: {
alignItems: 'center',
},
parentItem: {
flexDirection: 'row',
alignItems: 'center',
borderBottomWidth: 1,
borderBottomColor: '#F0F0F0',
paddingTop: 4,
paddingBottom: 4,
},
title: {
margin: 16,
fontWeight: 'bold',
color: '#F0F0F0',
textAlign: 'center',
},
backButtonRow: {
flexDirection: 'row',
alignItems: 'center',
paddingBottom: 17,
paddingLeft: 3,
borderBottomColor: '#F0F0F0',
borderBottomWidth: 1,
},
backButtonText: {
marginLeft: 10,
color: '#F0F0F0',
},
});
export default CustomDrawerContent;
Then we add two different settings screen in app.js:
...
function Settings1Screen() {
return (
<View style={{flex: 1, alignItems: 'center', justifyContent: 'center'}}>
<Text>Settings 1 Screen</Text>
</View>
);
}
function Settings2Screen() {
return (
<View style={{flex: 1, alignItems: 'center', justifyContent: 'center'}}>
<Text>Settings 2 Screen</Text>
</View>
);
}
function MainDrawerNavigation() {
return (
<Drawer.Navigator
initialRouteName="Home"
drawerContent={(props) => (
<CustomDrawerContent drawerItems={drawerItemsMain} {...props} />
)}>
<Drawer.Screen name="Home" component={HomeScreen} />
<Drawer.Screen name="Settings1" component={Settings1Screen} />
<Drawer.Screen name="Settings2" component={Settings2Screen} />
</Drawer.Navigator>
);
}
$ npx react-native run-android
We have now a fully nested multi-level drawer in our react-native app! :)
Hope it will help you!
Have fun :)
Top comments (2)
It works !! Thank you so much!
Manu bar Home Press then TypeError: undefined is not an object (evaluating 'filteredMainDrawerRoutes.routes.length')
Plaese Help me