React Native Navigation: Bottom Tabs, Drawer, Nested Stacks, and a Reusable Modal HOC

React Native Navigation: Bottom Tabs, Drawer, Nested Stacks, and a Reusable Modal HOC

React Native Navigation: Bottom Tabs, Drawer, Nested Stacks, and a Reusable Modal HOC Most production React Native apps need more than one navigator. A common structure is: a splash screen at the app root; a drawer for top-level destinations; bottom tabs for the main sections; a stack inside each tab for detail screens; and reusable dialogs that do not require a navigation route. This article walks through that architecture and explains when to use a navigation modal versus a reusable Higher-Order Component (HOC) modal. What we are building flowchart TD App[App] --> Root[Root Stack] Root --> Splash[Splash] Root --> Drawer[Drawer Navigator] Root --> RouteModal[Modal route] Drawer --> Tabs[Bottom Tab Navigator] Tabs --> Home[Home Stack] Tabs --> Explore[Explore Stack] Tabs --> Favorites[Favorites Stack] Tabs --> Profile[Profile Stack] Home --> HomeScreen[Home] Home --> Details[Details] Enter fullscreen mode Exit fullscreen mode The resulting navigation path looks like this: Root Stack → Drawer Navigator → Bottom Tabs → Individual Stack Enter fullscreen mode Exit fullscreen mode This keeps global navigation concerns outside of feature-specific detail screens. 1. Install the required packages Install React Navigation and the native dependencies: npm install @react-navigation/native \ @react-navigation/native-stack \ @react-navigation/bottom-tabs \ @react-navigation/drawer \ react-native-gesture-handler \ react-native-reanimated \ react-native-screens \ react-native-safe-area-context Enter fullscreen mode Exit fullscreen mode For iOS, install pods afterward: npx pod-install ios Enter fullscreen mode Exit fullscreen mode Import Gesture Handler before other app imports in index.js: import 'react-native-gesture-handler'; Enter fullscreen mode Exit fullscreen mode Also wrap the application with GestureHandlerRootView. It avoids gesture problems with drawers, especially on Android: import {GestureHandlerRootView} from 'react-native-gesture-handler'; export default function App() { return ( ); } Enter fullscreen mode Exit fullscreen mode Ensure the Reanimated Babel plugin required by your installed Reanimated version is configured as the final Babel plugin. Check the current Reanimated installation guide because the setup differs by version. 2. Centralize route names Nested navigation becomes easier to maintain when route names live in one file. export const Routes = { Splash: 'Splash', MainDrawer: 'MainDrawer', MainTabs: 'MainTabs', Modal: 'Modal', HomeTab: 'HomeTab', ExploreTab: 'ExploreTab', FavoritesTab: 'FavoritesTab', ProfileTab: 'ProfileTab', Home: 'Home', Details: 'Details', Explore: 'Explore', ExploreDetails: 'ExploreDetails', Favorites: 'Favorites', FavoriteDetails: 'FavoriteDetails', Profile: 'Profile', Settings: 'Settings', }; Enter fullscreen mode Exit fullscreen mode Using constants prevents subtle bugs caused by route-name typos. 3. Create a stack for each tab Each tab owns its own history. For example, the Explore tab can open an Explore Details screen without affecting the history of Home or Favorites. import {createNativeStackNavigator} from '@react-navigation/native-stack'; import {Routes} from '../../constants'; import {ExploreScreen, ExploreDetailsScreen} from '../../screens'; const Stack = createNativeStackNavigator(); export function ExploreStackNavigator() { return ( ); } Enter fullscreen mode Exit fullscreen mode Repeat this pattern for Home, Favorites, and Profile. 4. Add the bottom tab navigator The tab navigator hosts the four feature stacks. In this app, it also owns a shared custom header. import {createBottomTabNavigator} from '@react-navigation/bottom-tabs'; import {Routes} from '../constants'; import { HomeStackNavigator, ExploreStackNavigator, FavoritesStackNavigator, ProfileStackNavigator, } from './stacks'; import {AppHeader} from './AppHeader'; const Tab = createBottomTabNavigator(); export function TabNavigator() { return ( , tabBarHideOnKeyboard: true, }}> ); } Enter fullscreen mode Exit fullscreen mode Each tab now has independent stack state. A user can open a detail screen in Explore, switch to Favorites, then return to the same Explore detail screen. 5. Put tabs inside a custom drawer The drawer contains one screen—MainTabs—but its custom content can switch the nested active tab. const openTab = route => { navigation.navigate(Routes.MainTabs, {screen: route}); navigation.closeDrawer(); }; Enter fullscreen mode Exit fullscreen mode This is the important nested-navigation call: navigation.navigate(Routes.MainTabs, { screen: Routes.ExploreTab, }); Enter fullscreen mode Exit fullscreen mode A simple drawer setup looks like this: import {createDrawerNavigator} from '@react-navigation/drawer'; import {Routes} from '../constants'; import {TabNavigator} from './TabNavigator'; const Drawer = createDrawerNavigator(); export function DrawerNavigator() { return ( } screenOptions={{ headerShown: false, drawerType: 'front', swipeEdgeWidth: 80, }}> ); } Enter fullscreen mode Exit fullscreen mode Open the drawer from a tab header The tab navigator is a child of the drawer navigator. Therefore, the header can call getParent() to access drawer methods: function openMenu(navigation) { navigation.getParent()?.openDrawer(); } Enter fullscreen mode Exit fullscreen mode Use optional chaining only when a missing parent is an acceptable failure. For a required navigator relationship, validating the tree during development is better than silently doing nothing. 6. Add a root stack around everything The root navigator contains the splash screen, the main drawer application, and route-based modals. import {NavigationContainer} from '@react-navigation/native'; import {createNativeStackNavigator} from '@react-navigation/native-stack'; const Stack = createNativeStackNavigator(); export function RootNavigator() { return ( ); } Enter fullscreen mode Exit fullscreen mode The splash screen can enter a specific nested destination with replace, so users cannot navigate back to splash: navigation.replace(Routes.MainDrawer, { screen: Routes.MainTabs, params: {screen: Routes.HomeTab}, }); Enter fullscreen mode Exit fullscreen mode 7. Open a navigation modal from the drawer The drawer is nested inside the root stack. To open the root-level modal, move up one level: navigation.getParent()?.navigate(Routes.Modal, { eyebrow: 'NEED A HAND?', title: 'How can we help?', message: 'Browse the app from the drawer or contact support.', confirmLabel: 'Got it', }); Enter fullscreen mode Exit fullscreen mode Use a navigation modal when it is a destination in your app flow—for example, an onboarding step, a paywall, or a screen that needs route parameters and back-navigation behavior. 8. Build a reusable modal HOC Not every dialog needs to be a screen in the navigation tree. Confirmation dialogs, short feedback messages, and quick actions are often better as local UI state. The HOC below injects openModal and closeModal into a wrapped screen: import React, {useCallback, useState} from 'react'; import {AppModal} from './AppModal'; const INITIAL_MODAL = { visible: false, eyebrow: 'QUICK ACTION', title: 'Are you sure?', message: '', confirmLabel: 'Continue', cancelLabel: 'Not now', icon: '✦', onConfirm: undefined, }; export function withModal(WrappedComponent) { function ComponentWithModal(props) { const [modal, setModal] = useState(INITIAL_MODAL); const closeModal = useCallback(() => { setModal(current => ({...current, visible: false})); }, []); const openModal = useCallback((options = {}) => { setModal({...INITIAL_MODAL, ...options, visible: true}); }, []); const confirmModal = useCallback(() => { modal.onConfirm?.(); closeModal(); }, [closeModal, modal]); return ( <> ); } return ComponentWithModal; } Enter fullscreen mode Exit fullscreen mode Wrap a screen when exporting it: function HomeScreen({openModal}) { return ( openModal({ title: 'Reusable HOC modal', message: 'This dialog is controlled by the screen.', confirmLabel: 'Great', }) } /> ); } export default withModal(HomeScreen); Enter fullscreen mode Exit fullscreen mode The presentational modal uses React Native's built-in Modal: {/* Backdrop, card, and actions */} Enter fullscreen mode Exit fullscreen mode Navigation modal vs HOC modal Use a root navigation modal when… Use the HOC modal when… It is part of the navigation flow. It is local UI feedback or a confirmation. It needs route parameters or a deep-linkable destination. The screen simply needs openModal() state. Back navigation should dismiss it. You want to avoid adding a route for a small dialog. Multiple features must navigate to it consistently. The behavior is scoped to a particular wrapped screen. In this project, the help dialog is a root navigation modal, while the Home screen uses the reusable HOC modal. Important improvements before production Provide back navigation for detail screens. If the header is owned by the tab navigator while nested stacks hide their headers, detail screens need their own visible back action. Use GestureHandlerRootView. It is recommended for reliable drawer and gesture behavior. Keep one source of truth for modal behavior. The two modal approaches are both valid, but share styles and accessibility behavior where possible. Wrap only the screens that need the HOC. openModal is not global; each screen needs to be wrapped, or you can move to Context for global dialogs. Use an imperative navigation service carefully. A navigation ref is useful for push notifications and auth redirects, but screen components should normally use the navigation prop or hook. Final structure src/ ├── constants/ │ └── routes.js ├── components/ │ └── AppModal/ │ ├── AppModal.js │ └── withModal.js ├── navigation/ │ ├── RootNavigator.js │ ├── DrawerNavigator.js │ ├── TabNavigator.js │ └── stacks/ │ ├── HomeStack.js │ ├── ExploreStack.js │ ├── FavoritesStack.js │ └── ProfileStack.js └── screens/ └── Modal/ └── ModalScreen.js Enter fullscreen mode Exit fullscreen mode This architecture scales well because each layer has one responsibility: the root owns application flow, the drawer exposes major destinations, tabs organize primary sections, stacks handle local history, and the HOC handles local dialogs. What navigator combination are you using in your React Native app: tabs and stacks only, or a drawer as well?

📰 Original Source

Read full article at Dev →

KhanList aggregates and links to publicly available news content. We do not host full articles from third-party sources. Always verify important information with original sources.