In a React Native metro.config.js, how do getDefaultConfig and mergeConfig work together, and how do you add an extra asset extension such as .glb safely?
answer
- defaults first, your changes on top
- sections merged one level deep
- arrays and functions replace, not append
- spread defaultConfig.resolver.assetExts
- Expo: mutate getDefaultConfig's result
basics
~20 sgetDefaultConfig returns React Native's complete Metro defaults and mergeConfig overlays your changes section by section. Arrays are replaced, not appended, so add .glb by spreading the default assetExts plus 'glb', never by passing ['glb'] alone.
solid answer
~40 s`getDefaultConfig(__dirname)` from `@react-native/metro-config` returns Metro's defaults with React Native's layered on top: platforms, asset registry, Babel transformer, polyfills, `inlineRequires`. `mergeConfig(base, ...overrides)` merges each section (`resolver`, `transformer`, `serializer`, `server`, `watcher`) key by key, but a key's value is replaced wholesale: arrays and functions are not deep-merged. So `mergeConfig(defaultConfig, {resolver: {assetExts: ['glb']}})` leaves `glb` as the only asset extension and every `png` import breaks. The safe form reads the defaults first: `assetExts: [...defaultConfig.resolver.assetExts, 'glb']`. React Native's docs also accept writing the full list out explicitly so the config is the source of truth. In Expo, `getDefaultConfig` from `expo/metro-config` returns a fresh object and the documented pattern is `config.resolver.assetExts.push('glb')`. Restart Metro afterwards.
code
javascript · 11 lines// metro.config.js (React Native 0.87 CLI project)
const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
const defaultConfig = getDefaultConfig(__dirname);
module.exports = mergeConfig(defaultConfig, {
resolver: {
// Spread the defaults: mergeConfig replaces arrays, it does not append.
assetExts: [...defaultConfig.resolver.assetExts, 'glb'],
},
});go deeper
Recall that the config starts from getDefaultConfig and that new asset types go into resolver.assetExts.
Explain mergeConfig's section-level merge and array replacement, and write the spread form that keeps the default extensions.
Recognise the symptom of a replaced array, unrelated assets failing to resolve, and keep overrides minimal so upgrades bring new defaults in.
Decide whether the team spreads defaults or writes explicit lists, weighing a single source of truth against silently missing new defaults on upgrade.
## The scenario A product screen shows a 3D model shipped as `assets/chair.glb`. `require('./assets/chair.glb')` fails because Metro does not know `.glb` is an asset. Metro only treats files as assets when their extension is in `resolver.assetExts`, and `glb` is not in its default list. The fix is one extension, but how you add it decides whether the rest of the app keeps working. ## What getDefaultConfig returns In a React Native CLI project, `getDefaultConfig(projectRoot)` from **`@react-native/metro-config`** builds the complete configuration: 1. Metro's own defaults (`sourceExts`, `assetExts`, workers, cache, minifier); 2. React Native's layer on top: `platforms: ['android', 'ios']`, the React Native asset registry, `@react-native/metro-babel-transformer`, React Native's polyfills and setup module, `inlineRequires: true`, `server.port` from `RCT_METRO_PORT` or 8081. The React Native CLI needs a config that extends this package. Since 0.73 it warns that a config which does not "will fail to build". ## How mergeConfig merges `mergeConfig(base, ...configs)` applies each config over the previous result, right-most winning. Its rules are specific: - **Top-level keys** are replaced. - **Sections** (`resolver`, `transformer`, `serializer`, `server`, `watcher`, `symbolicator`) are merged **one level deep**: keys you do not mention survive. - **Values inside a section are replaced wholesale.** Metro's docs say it directly: arrays and function-valued options do not deep-merge. - An argument may be a **function** that receives the merged config so far and returns a partial config, which is the tidy way to extend an array. - If any argument is a promise or async function, `mergeConfig` returns a promise. ## The trap and the fixes | Code | Result | |---|---| | `resolver: {assetExts: ['glb']}` | only `glb` is an asset; `png`, `ttf`, `mp4` imports stop resolving | | `resolver: {assetExts: [...defaultConfig.resolver.assetExts, 'glb']}` | defaults kept, `glb` added | | `config => ({resolver: {assetExts: [...config.resolver.assetExts, 'glb']}})` | same, using the function form | | full list written out by hand | works; React Native's docs recommend this so the config is the source of truth, but it must be revisited on upgrades | The first row is the classic mistake: it looks like "add glb" but it means "assets are only glb". The symptom appears far from the change, as images or fonts failing to resolve. ## The Expo version In an Expo project the base is **`getDefaultConfig` from `expo/metro-config`** (import it from `expo/metro-config` rather than `@expo/metro-config`, so the version matches the installed Expo SDK). It returns a fresh object each call, and Expo documents mutating it: ```js const {getDefaultConfig} = require('expo/metro-config'); const config = getDefaultConfig(__dirname); config.resolver.assetExts.push('glb'); module.exports = config; ``` Expo's defaults already add some asset extensions for its own modules, such as `db` for `expo-sqlite` and `heic` and `avif` for `expo-image`, so check the list before adding one that is already there. ## What the default asset list gives you Metro's default `assetExts` covers images (`png`, `jpg`, `gif`, `webp`, `svg` and more), video, audio, fonts (`ttf`, `otf`), documents and `zip`. Assets also get scale handling: `assetResolutions` defaults to `1`, `1.5`, `2`, `3` and `4`, so `[email protected]` and `[email protected]` are picked per screen density. Replacing the array by accident removes all of that at once, which is why the failure looks so much bigger than the change that caused it. While debugging, a temporary `console.log(config.resolver.assetExts)` at the end of `metro.config.js` shows the merged list Metro will actually use. ## Finishing the job - **Restart Metro**: the config is read once at startup. - **Type the import** if the project uses TypeScript: a `declare module '*.glb'` declaration. - **Check the consumer**: an asset import yields an asset reference; the library that loads the model decides how to use it. ## What interviewers probe A middle answer knows that the defaults come from the framework package, that `mergeConfig` merges sections but replaces arrays, and can write the one-line safe version from memory. The follow-up is usually the Expo difference, or why a config that "just adds glb" broke every image in the app.
- After a config change that 'only added glb', every PNG import in the app fails. What happened?The config passed `assetExts: ['glb']` through `mergeConfig`. Sections merge key by key, but an array value replaces the default array, so `png` and every other asset extension disappeared. Spread the default list and add `glb`, then restart Metro.
- When is mergeConfig's function form useful?When a later override needs the merged value so far, for example appending to an array another override already extended. Each function receives the config merged from everything to its left and returns a partial config, which avoids reading defaults into separate variables.
saying these in an interview costs you the question
- mergeConfig deep-merges arrays, so passing ['glb'] appends it
- A metro.config.js can skip getDefaultConfig in a React Native CLI app
- Adding an asset extension requires a native rebuild
- Expo projects should import getDefaultConfig from @expo/metro-config
- Every Expo project must add 'db' before bundling SQLite files