How does getDefaultConfig from expo/metro-config differ from the one in @react-native/metro-config, and which should a project extend?
answer
- extend the one matching your CLI
- RN CLI: file required, warns if not extended
- Expo: file optional, npx expo customize
- Expo swaps worker, Babel transformer, serializers
- inlineRequires true vs false
basics
~20 sExtend the package that matches the CLI running Metro. @react-native/metro-config adds React Native's defaults for the React Native CLI; expo/metro-config builds on Metro's defaults with Expo's transformer, serializers, extra extensions and monorepo detection, and Expo CLI expects it.
solid answer
~40 sBoth return a complete Metro config, but for different tools. `@react-native/metro-config` adds React Native's essentials to Metro's defaults: `android` and `ios` platforms, the asset registry, `@react-native/metro-babel-transformer`, polyfills and `inlineRequires: true`. The React Native CLI requires a `metro.config.js` and warns if it does not extend this package. `expo/metro-config` is what Expo CLI expects: its own transform worker and Babel transformer (so `babel-preset-expo` always applies), Expo serializers, CSS and extra asset extensions, `require.context` for Expo Router, monorepo detection that sets `watchFolders` and `nodeModulesPaths`, and `inlineRequires: false` with `experimentalImportSupport: true`. In Expo the file is optional: `npx expo customize metro.config.js` generates it, and you import from `expo/metro-config` so the version matches the SDK. Mixing them, such as an Expo app extending the React Native package, drops Expo's pieces and breaks features.
code
javascript · 11 lines// metro.config.js in an Expo SDK 57 project
// generated with: npx expo customize metro.config.js
const {getDefaultConfig} = require('expo/metro-config');
/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname);
// Change the returned object in place.
config.resolver.assetExts.push('glb');
module.exports = config;go deeper
Recall that bare React Native CLI apps extend @react-native/metro-config and Expo apps extend expo/metro-config in metro.config.js.
Name concrete differences, such as Expo's transformer and serializers, the inlineRequires defaults and the optional Expo file, and explain why mixing them breaks Expo features.
Audit inherited configs after migrations between CLI and Expo, removing overrides that the chosen framework now handles, such as monorepo watchFolders.
Treat the Metro base package as part of the framework choice; moving a bare app to Expo CLI is a build-pipeline change, not a one-line import swap.
## Two starting points for one bundler Every React Native app is bundled by **Metro**, but Metro alone knows nothing about React Native: it does not know the platforms, where assets register, which Babel preset to use, or which polyfills the runtime needs. Two packages supply those defaults: - **`@react-native/metro-config`**: the React Native team's defaults, used by the React Native CLI (`npx react-native start`, `run-android`, `run-ios`). - **`expo/metro-config`** (a re-export of `@expo/metro-config`): Expo's defaults, used by Expo CLI (`npx expo start`, `npx expo export`) and by EAS builds that call it. The rule is simple: **extend the package that matches the CLI that runs Metro.** ## What each adds | | `@react-native/metro-config` | `expo/metro-config` | |---|---|---| | Transform worker | Metro's default | Expo's own worker | | Babel transformer | `@react-native/metro-babel-transformer` | Expo's, always applying `babel-preset-expo` | | `inlineRequires` | `true` | `false` | | `experimentalImportSupport` | `false` | `true` | | Extra asset extensions | none beyond Metro's | e.g. `db`, `heic`, `avif` for Expo modules | | Extra source extensions | none beyond Metro's | CSS and Sass when CSS support is on | | `require.context` | off | on, used by Expo Router | | Monorepos | configure `watchFolders` yourself | detects workspaces and sets `watchFolders` and `nodeModulesPaths` | | Serializers | Metro's | Metro's plus Expo serializer plugins | | Dev server port | `RCT_METRO_PORT` or 8081 | `RCT_METRO_PORT` or 8081 | ## How each CLI treats the file **React Native CLI.** The CLI requires a Metro config file in the project root and throws "No Metro config found" without one. The template ships `mergeConfig(getDefaultConfig(__dirname), config)`. If the loaded config never called `getDefaultConfig` from `@react-native/metro-config`, the CLI prints a warning that has existed since 0.73: your Metro config should extend `@react-native/metro-config` "or it will fail to build". **Expo CLI.** The file is **optional**: with no config file, Expo CLI uses its default config. When you need one, `npx expo customize metro.config.js` generates the template, which calls `getDefaultConfig(__dirname)` from `expo/metro-config` and exports the result. Expo's guidance, in order: 1. Import from **`expo/metro-config`**, not `@expo/metro-config`, so the version always matches the installed `expo` package. 2. Change the returned object in place, for example `config.resolver.assetExts.push(...)`. 3. Expect some options to be locked down. Expo documents that not every upstream Metro option can be customised, and it does not load Metro configs from YAML or from outside the repository. 4. In a monorepo, delete manual `watchFolders` and `nodeModulesPaths` overrides: Expo configures them automatically. ## What stays the same Both packages start from Metro's own defaults and both use Metro's `mergeConfig` internally, so the section-by-section merge rules are identical. Both read `RCT_METRO_PORT` for the development server port and fall back to 8081. Both resolve the `react-native` field of a package before `browser` and `main`. And both expect the config file to live in the project root and pass `__dirname` to `getDefaultConfig`. The differences are in what each framework layers on top, not in how Metro itself behaves. ## What goes wrong when they are mixed - An **Expo app extending `@react-native/metro-config`** loses Expo's transformer and serializers. Expo Router's file-based routes, CSS support and Expo's web and server output depend on them, so these break in confusing ways. - A **bare CLI app extending `expo/metro-config`** runs with Expo's assumptions, including `inlineRequires: false`. That can work in a bare app that uses Expo modules and Expo CLI, but it should be a deliberate choice, not an accident. - **Copying snippets across ecosystems** is the common cause: a resolver snippet written for `mergeConfig` in a CLI app is pasted into an Expo config that mutates the default object, or the other way round. ## A practical way to decide 1. Which command starts Metro in development: `npx expo start` or `npx react-native start`? 2. Use that ecosystem's package, and its documented pattern for changes. 3. Keep customisation small: every override of a framework default is something to re-check on the next upgrade. ## Interview framing At middle level, interviewers want the rule (match the CLI), two or three concrete differences (Expo's transformer and `babel-preset-expo`, the `inlineRequires` defaults, the optional file and `npx expo customize`) and awareness that mixing them breaks Expo Router and other Expo features.
- Why does Expo tell you to import from expo/metro-config rather than @expo/metro-config?`expo/metro-config` re-exports the `@expo/metro-config` version that ships with the installed `expo` package. Importing `@expo/metro-config` directly can resolve a different version than the SDK expects, so the defaults and the CLI drift apart.
- A monorepo Expo app has a hand-written watchFolders list from an old setup. What should you do?Expo's monorepo guide says to delete manual `watchFolders` and `nodeModulesPaths` overrides when using `expo/metro-config`, because it detects workspaces and sets them itself. Then start once with `npx expo start --clear` so stale cache entries are dropped.
saying these in an interview costs you the question
- Both packages give identical defaults, so either works anywhere
- An Expo project must always contain a metro.config.js
- The React Native CLI runs fine with no Metro config file
- Expo apps should extend @react-native/metro-config for stability
- Expo and React Native CLI apps both inline requires by default