skip to content

In an Expo SDK 57 app on Expo Router, why does importing ThemeProvider from '@react-navigation/native' fail to bundle, and what replaces it?

level: seniorimportance: should knowfreq 30%

answer

  1. SDK 56 breaking change
  2. expo-router vendors the navigation code
  3. Metro resolver rejects @react-navigation/*
  4. expo-router/react-navigation, js-stack, js-tabs
  5. a codemod does the rewrite

basics

~10 s

Since SDK 56 expo-router ships its own copy of the React Navigation code and Expo CLI's Metro resolver rejects @react-navigation/* imports from app code; import ThemeProvider from 'expo-router/react-navigation' (or 'expo-router') instead.

solid answer

~40 s

In SDK 56 `expo-router` removed its dependency on React Navigation and moved that code inside the package. Expo CLI's Metro resolver now throws when **application code** (anything outside `node_modules`) imports `@react-navigation/*`, with an error that expo-router is no longer compatible with react-navigation as of SDK 56. The runtime API is unchanged; only the module specifiers move: `@react-navigation/native`, `core`, `elements` and `routers` map to `expo-router/react-navigation`, `stack` to `expo-router/js-stack`, `bottom-tabs` to `expo-router/js-tabs`, `material-top-tabs` to `expo-router/js-top-tabs`, and native-stack and drawer have no import equivalent, you use the `Stack` and `Drawer` layouts. `npx expo-codemod sdk-56-expo-router-react-navigation-replace src` rewrites the imports. Libraries importing `@react-navigation/core` from `node_modules` are rewritten automatically for now.

code

tsx · 15 lines
tsx
// src/app/_layout.tsx (Expo SDK 57)
import { DarkTheme, DefaultTheme, ThemeProvider } from 'expo-router/react-navigation';
import { Stack } from 'expo-router';
import { useColorScheme } from 'react-native';

export default function RootLayout() {
  const scheme = useColorScheme();
  return (
    <ThemeProvider value={scheme === 'dark' ? DarkTheme : DefaultTheme}>
      <Stack>
        <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
      </Stack>
    </ThemeProvider>
  );
}

go deeper

for a junior

Recall that on Expo SDK 56 and later, navigation types, hooks and themes are imported from expo-router entry points, not from @react-navigation packages.

for a middle

Explain the import map, including that native-stack and drawer become the Stack and Drawer layouts, and that the root Tabs export is deprecated for expo-router/js-tabs.

for a senior

Plan the SDK upgrade: run the codemod, audit layouts, know the node_modules shim only covers @react-navigation/core and is temporary, and avoid the disable variable.

for a principal

Weigh the coupling cost of a router that vendors its navigation layer against owning navigation directly, including how library ecosystems follow such moves.

## What changed in SDK 56 Up to SDK 55, Expo Router was built **on top of** the React Navigation packages: it depended on `@react-navigation/native` and friends, and app code imported themes, hooks and navigator types straight from them. In **SDK 56** (expo-router 56.0.0) that changed: the changelog's breaking change reads "Remove dependency on react-navigation and move the navigation code to expo-router". The navigation code now lives **inside** `expo-router`, exposed through its own entry points. SDK 57, the current target, keeps that model. ## The error you see When the Expo CLI builds the bundle, its Metro resolver checks every import. If expo-router is installed and a file **outside `node_modules`** imports a module starting with `@react-navigation/`, resolution fails: - For `@react-navigation/native-stack` or `@react-navigation/drawer`, the error explains that expo-router is no longer compatible with react-navigation and tells you to use `Stack` from `expo-router` or `Drawer` from `expo-router/drawer`. - For any other `@react-navigation/*` package, it throws the same compatibility message with a link to the SDK 55 to 56 migration guide. The error protects you from a subtle bug: an external React Navigation package would be a **separate copy** of that code, and providers or hooks from it would not share state with the navigators Expo Router renders. ## The import map The runtime API is unchanged, so migration is a find-and-replace of **module specifiers**: | Old import | SDK 56+ import | |---|---| | `@react-navigation/native` | `expo-router/react-navigation` | | `@react-navigation/core` | `expo-router/react-navigation` | | `@react-navigation/elements` | `expo-router/react-navigation` | | `@react-navigation/routers` | `expo-router/react-navigation` | | `@react-navigation/stack` | `expo-router/js-stack` | | `@react-navigation/bottom-tabs` | `expo-router/js-tabs` | | `@react-navigation/material-top-tabs` | `expo-router/js-top-tabs` | | `@react-navigation/native-stack` | no import; use the `Stack` layout | | `@react-navigation/drawer` | no import; use the `Drawer` layout | The common theme pieces (`ThemeProvider`, `DarkTheme`, `DefaultTheme`, `useTheme`) are also re-exported from the `expo-router` root. In SDK 57 the root `Tabs` export is marked **deprecated** in favour of `expo-router/js-tabs`, so new layouts should import tabs from there. ## Libraries in node_modules Many third-party packages still import `@react-navigation/core`. As a **temporary compatibility shim**, Expo CLI rewrites `@react-navigation/core` imports that originate in `node_modules` to `expo-router/react-navigation`, so those libraries keep working without edits. Your own code gets no such rewrite: it gets the error. The environment variable `EXPO_ROUTER_DISABLE_RN_NAVIGATION_CHECK=1`, set before `npx expo start`, turns off **both** the error and the rewrite, which is an escape hatch for debugging rather than a fix. ## Migrating the recipe app 1. Run `npx expo-codemod sdk-56-expo-router-react-navigation-replace src` from the project root, pointing it at the folder that holds your code. 2. Check the root `_layout.tsx`: `ThemeProvider` and `DarkTheme` should now come from `expo-router/react-navigation` or `expo-router`. 3. Replace any hand-built native-stack or drawer navigator with the `Stack` or `Drawer` layout, because those packages have no import equivalent. 4. Move `Tabs` imports to `expo-router/js-tabs`. 5. Remove the `@react-navigation/*` packages your code no longer imports from `package.json`, keeping any a library still needs. 6. Start the bundler and confirm no resolution errors remain. ## Checks after the upgrade Once the bundle builds, a few checks catch what the codemod cannot: - **Types**: type-only imports from `@react-navigation/*` are erased before bundling, so Metro never flags them, yet they still describe a separate package; move them to the router's entry points with the value imports. - **Tests**: Jest mocks or module maps that referenced `@react-navigation/native` by name need the new specifiers, or tests resolve a different module than the app. - **Direct navigator construction**: any `create...Navigator` call for a native stack or drawer must become a `Stack` or `Drawer` layout, since those packages have no import equivalent. - **Dependencies**: `@react-navigation/*` packages kept only for your own imports can go, which shrinks the dependency tree and removes version-skew risk. - **Library behaviour**: a library that imports anything other than `@react-navigation/core` from `node_modules` is not covered by the automatic rewrite, so verify it on device. ## Why it comes up in interviews Upgrading across an SDK is routine senior work, and this change breaks bundling on day one of an SDK 56 or 57 upgrade for any app that touched React Navigation directly. A good answer explains the mechanism (vendored code, one copy, a resolver check), names the map, and knows the node_modules shim is temporary.

  • Why does a third-party library that imports @react-navigation/core keep working after the SDK 56 upgrade?
    Expo CLI rewrites `@react-navigation/core` imports that come from `node_modules` to `expo-router/react-navigation`, so the library uses the router's copy. Expo calls this a temporary shim and plans a migration guide for library authors.
  • What does EXPO_ROUTER_DISABLE_RN_NAVIGATION_CHECK=1 change?
    It disables the resolver check entirely: app code may import `@react-navigation/*` without an error, and the automatic rewrite for libraries in `node_modules` also stops. It is a debugging escape hatch, not a migration.
  • How do you replace a createNativeStackNavigator call in an Expo Router app on SDK 57?
    There is no import equivalent for `@react-navigation/native-stack`. Use the `Stack` layout from `expo-router` in a `_layout.tsx`, which renders a native stack and takes the same screen options.

saying these in an interview costs you the question

  • Expo Router still depends on @react-navigation/native, so both imports work.
  • The migration changes the navigation API, not just import paths.
  • Expo CLI silently rewrites every @react-navigation import in app code.
  • @react-navigation/native-stack maps to an expo-router/native-stack import.
  • Setting the disable-check variable is the supported long-term fix.