In a React Native project, what does babel.config.js control, and how does babel-preset-expo differ from @react-native/babel-preset?
answer
- presets and plugins for every file
- module:@react-native/babel-preset in bare apps
- Expo: optional file, npx expo customize
- babel-preset-expo extends the RN preset
- restart Metro after editing
basics
~10 sbabel.config.js lists the Babel presets and plugins Metro applies to every file. Bare apps use @react-native/babel-preset; Expo apps use babel-preset-expo, which extends it with Expo-specific transforms, and the file is optional in Expo.
solid answer
~40 s`babel.config.js` tells Metro's Babel transformer which presets and plugins to run on every file in the graph, including `node_modules`. A bare React Native 0.87 template ships one with `presets: ['module:@react-native/babel-preset']`; that preset compiles JSX, TypeScript and Flow, and targets Hermes. An Expo project uses `babel-preset-expo`, which extends `@react-native/babel-preset` and adds Expo's own transforms, such as decorators support, web and server environment handling, and React Compiler options. In Expo the file is optional: without it the preset still applies, and `npx expo customize babel.config.js` generates one when you need a plugin. Expo's Metro config also installs its own Babel transformer so that preset is always used. Don't replace the preset in an Expo app, and restart Metro after editing the file because transformed output is cached.
code
javascript · 17 lines// babel.config.js in an Expo SDK 57 project,
// generated with: npx expo customize babel.config.js
module.exports = function (api) {
api.cache(true);
return {
presets: [
[
'babel-preset-expo',
{
'react-compiler': {
sources: (filename) => filename.includes('src/features'),
},
},
],
],
};
};go deeper
Recall which preset each kind of project uses and that babel.config.js lists the presets and plugins Metro's Babel pass applies.
Explain that babel-preset-expo extends @react-native/babel-preset, that the Expo file is optional, and why an edit needs a Metro restart.
Diagnose preset mix-ups after upgrades or template changes, and keep library-required plugins in the right order without duplicating presets.
Treat the Babel config as shared build surface: every plugin runs on every file, so each addition costs build time and must survive upgrades.
## What babel.config.js is for **Babel** is the compiler that turns the code you write (JSX, TypeScript, Flow, recent JavaScript syntax) into JavaScript the app's engine can run. In React Native you do not run Babel yourself: **Metro** runs it on every file, one file at a time, as part of its transformation stage. `babel.config.js` is where you tell that Babel pass what to do: - **presets**: bundles of plugins that describe a whole target. You normally have exactly one. - **plugins**: individual transforms you add on top, usually because a library's install instructions say so. - **options**: settings passed to a preset or plugin, written as `[name, options]` pairs. Because the config applies to every file in the graph, including `node_modules`, it affects dependencies as well as your own code. ## The bare React Native preset A project created from the React Native 0.87 template ships this file: ```js module.exports = { presets: ['module:@react-native/babel-preset'], }; ``` The `module:` prefix tells Babel to resolve the preset as a package name. **`@react-native/babel-preset`**: - strips TypeScript and Flow types and compiles JSX; - converts `import`/`export` to CommonJS unless Metro is told to handle imports itself; - runs the codegen plugin on native component specs; - targets **Hermes**: its transform profile defaults to `hermes-stable`, and since 0.84 it no longer carries the extra transforms for other engines. It replaced the older `metro-react-native-babel-preset` in React Native 0.73; that name belongs only in old projects. If the file is missing, Metro's default Babel transformer (`@react-native/metro-babel-transformer`) falls back to this preset anyway. ## The Expo preset **`babel-preset-expo`** is the default in every project created with `npx create-expo-app`. It **extends** `@react-native/babel-preset`, so everything above still applies, and adds Expo's own work: decorators support, transforms that depend on the bundling target (native, web or server rendering), and the React Compiler integration, configured through a `react-compiler` option on the preset. Three Expo specifics trip people up: 1. **The file is optional.** Expo applies `babel-preset-expo` even without a config file. Run `npx expo customize babel.config.js` to generate one only when you need to add a plugin or pass preset options. 2. **Expo's Metro config installs its own Babel transformer** (`@expo/metro-config/babel-transformer`), set through `transformer.babelTransformerPath`, to make sure the Expo preset and its web and server handling are always in place. 3. **Swapping the preset for `@react-native/babel-preset` removes those Expo transforms**, and Expo features that rely on them stop working. ## Comparing the two | | `@react-native/babel-preset` | `babel-preset-expo` | |---|---|---| | Default in | React Native CLI template | `create-expo-app` projects | | Config file | shipped in the template | optional; `npx expo customize babel.config.js` | | Relationship | the base preset | extends the base preset | | Extra transforms | none beyond React Native's | decorators, web and server targets, React Compiler options | | Metro Babel transformer | `@react-native/metro-babel-transformer` | `@expo/metro-config/babel-transformer` | ## Editing the file safely - **Keep one preset** and add plugins beside it. A library that needs a Babel plugin names it and says where it goes; plugins run in the order listed. - **Pass options as a pair**, for example `['babel-preset-expo', { 'react-compiler': { … } }]`, rather than adding a second copy of the preset. - **Restart Metro after every edit.** Metro caches transformed files, and a change to the Babel config is only reliably picked up with a clean start, which Expo documents as `npx expo start --clear`. - `api.cache(true)` in the function form tells Babel it may cache the evaluated config itself; it does not affect Metro's transform cache. ## A typical upgrade trap Older projects often still list `module:metro-react-native-babel-preset`, or carry plugins that later preset versions already include. When upgrading React Native, compare `babel.config.js` with the new template rather than keeping the old file: a stale preset name fails to resolve, and a duplicated plugin can apply the same transform twice. In an Expo project the same check applies after an SDK upgrade, and the simplest safe state is often no file at all. ## What interviewers listen for A middle-level answer says that the file configures the per-file Babel pass Metro runs, names the right preset for each kind of project, knows that the Expo preset builds on the React Native one rather than replacing it, and knows that a config change needs a restart. The weak answers treat `babel.config.js` as a build script, or try to "fix" an Expo project by switching it to the bare preset.
- An Expo app starts failing after someone replaced babel-preset-expo with @react-native/babel-preset. Why?`babel-preset-expo` extends the React Native preset and adds Expo's own transforms, such as decorators and the web and server handling. Switching to the base preset removes them, so code that depends on them breaks. Revert to `babel-preset-expo` and add any extra plugin beside it instead.
- You added a Babel plugin and nothing changed. What is the first thing to check?Whether Metro was restarted with a clean cache. Metro caches transformed output per file, so an edited Babel config is only reliably applied after a clean start, for example `npx expo start --clear` in an Expo project.
saying these in an interview costs you the question
- Every Expo project must have a babel.config.js or it will not bundle
- babel-preset-expo replaces the React Native preset with a different compiler
- Adding both presets side by side is the safe way to customise
- Babel config changes apply instantly without restarting Metro
- metro-react-native-babel-preset is the current preset name