Skip to main content

react-navigation Drawer: Custom drawerContent Gotchas

Two bugs found and fixed together in v1.8.219, both in apps/mobile-app/app/(shared)/dashboard/_layout.tsx's CustomDrawerContent (the component passed as <Drawer drawerContent={...}>). Neither produced a crash or an error — both failed silently, which is why they're worth documenting: nothing in the console pointed at the cause.

Bug 1: useNavigation() inside drawerContent resolves to the wrong navigator

Symptom: tapping the hamburger button did nothing. No error, no warning, no crash — dispatch(DrawerActions.openDrawer()) appeared to "succeed" (no exception thrown) but the drawer never visually opened.

Root cause: CustomDrawerContent called the useNavigation() hook to get a navigation object:

// Wrong — resolves to the nearest ancestor Stack navigator, not the Drawer
function CustomDrawerContent() {
const navigation = useNavigation<DrawerNavigation>();
...
}

@react-navigation/drawer explicitly passes navigation as a prop to whatever component renders as drawerContent — it's part of DrawerContentComponentProps, and it's guaranteed to be the Drawer navigator itself. useNavigation() resolves via React context lookup instead, and in this app's specific tree, that context lookup found the nearest ancestor Stack navigator, not the Drawer. react-navigation silently drops a dispatched action that the target navigator doesn't recognize (OPEN_DRAWER sent to a Stack navigator does nothing, no error) — hence the tap "working" with zero visible or logged effect.

Confirmed, not guessed: added a temporary log calling getState() on both the hook-obtained navigator and the prop-obtained one before fixing anything —

drawerNavRef state: {"type":"stack", ...} // hook — wrong
navigationFromContext state: {"type":"stack", ...} // same object!

— proving they were literally the same Stack navigator. After the fix:

drawerNavRef state: {"type":"drawer", "routeNames":["explore","notifications",...]}

Fix: use the prop, not the hook.

function CustomDrawerContent({ navigation }: DrawerContentComponentProps) {
// navigation here IS the Drawer navigator
}

Takeaway: any custom component rendered via drawerContent (or similarly, a custom header render prop, tabBarContent, etc.) receives the correctly-scoped navigation object as a prop for exactly this reason — reach for the prop before reaching for useNavigation(). The hook is for components that don't otherwise have access to it; components react-navigation renders directly into a slot usually already have it.

Bug 2: a sibling Header with elevated zIndex paints over the open drawer panel

Context: this app renders a custom Header as a sibling of <Drawer> on Android (not inside it), with position: absolute and zIndex: 1000, so screen content can scroll underneath it. See the comment block above ScreenWithHeader in _layout.tsx for why Header lives outside the Drawer's own subtree on Android specifically (a separate, older react-native-screens crash workaround).

Symptom: once Bug 1 was fixed and the drawer actually opened, its panel was visible but visually started below the Header row instead of covering the full screen — the Header appeared to sit on top of the open drawer.

What didn't work: dropping the Header's zIndex conditionally (isDrawerOpen ? 0 : 1000) while the drawer is open. Confirmed via log that isDrawerOpen was flipping correctly and the style was being applied — the Header still rendered on top regardless. React Native's zIndex reliably reorders siblings within the same stacking context, but Android specifically often needs a matching elevation too for genuine overlap reordering, and/or the Drawer's own open panel may render through a native surface that a plain sibling zIndex change can't out-rank.

What worked: don't render the Header at all while the drawer is open, instead of trying to out-stack it.

{Platform.OS === 'android' && !isDrawerOpen && <ScreenWithHeader />}

The Drawer's own content already has its own top branding/header, so nothing is visually lost — and this sidesteps the stacking question entirely rather than fighting it.

Getting isDrawerOpen into DashboardLayout: @react-navigation/drawer exports useDrawerStatus(), but it only works called from inside the Drawer's context — i.e., from CustomDrawerContent, not from DashboardLayout itself (which renders <Drawer>, and is therefore an ancestor, not a descendant, of the Drawer's own context). Lift it with a callback prop:

// CustomDrawerContent
const drawerStatus = useDrawerStatus();
useEffect(() => {
onDrawerStatusChange?.(drawerStatus === 'open');
}, [drawerStatus, onDrawerStatusChange]);

// DashboardLayout
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
// ...pass onDrawerStatusChange={setIsDrawerOpen} down via drawerContent

Side effect worth knowing about: with the Header hidden while the drawer is open, the hamburger button (part of Header) can't be tapped again to close the drawer on Android — there's nothing on screen to tap. Users close it via the standard drawer affordances instead: tapping the dimmed backdrop, swiping, or the Android back button — all handled by react-navigation's Drawer natively, unaffected by any of this. The onPress handler was changed from always DrawerActions.openDrawer() to DrawerActions.toggleDrawer() anyway, since on iOS Header renders inside the Drawer's own header slot (a different code path, drawerHeaderOptions in the same file) and stays reachable while the drawer is open — toggling is what makes the same button close it there.

How this was actually debugged

Static code reading didn't find either bug — both were found by adding temporary, clearly-marked console.log/getState() instrumentation, rebuilding a real release APK, and reading adb logcat while reproducing on an emulator. See local-android-debugging.md for the general workflow. Don't skip straight to a plausible-sounding fix for a silent failure like this — get the actual navigator state or event log first.