For a React Native Web build, how does a Babel alias to react-native-web differ from a bundler alias, and what does babel-plugin-react-native-web add?
answer
- rewrite at compile time, not resolve time
- module-resolver with a ^...$ pattern
- one import per export
- CommonJS kills tree shaking
- commonjs option for the dist path
basics
~20 sA Babel alias rewrites the import string during compilation, so any tool that runs Babel sees react-native-web. babel-plugin-react-native-web also rewrites each named import to its own module path, so unused components are dropped from the bundle.
solid answer
~40 sA bundler alias changes how a specifier **resolves**; a Babel alias changes the **source text** before the bundler sees it, so it works for any pipeline that runs Babel. React Native Web documents `babel-plugin-module-resolver` with `alias: { '^react-native$': 'react-native-web' }`. Its own `babel-plugin-react-native-web` goes further: it aliases the package *and* turns `import { StyleSheet, View } from 'react-native'` into one default import per export from `react-native-web/dist/exports/StyleSheet` and `.../View`. That matters because React Native's Babel preset converts ES modules to CommonJS, which stops bundlers from tree-shaking the library's index; per-export imports keep unused components out. Names the plugin does not know fall back to the package index. Set `commonjs: true` when the bundler consumes CommonJS so paths point at `dist/cjs`. The internal paths are not a stable API: only the plugin should write them.
go deeper
Recall that the alias can also be done in Babel, and that React Native Web ships a Babel plugin recommended for web builds.
Explain compile-time versus resolve-time aliasing, the anchored module-resolver pattern, and the plugin's per-export rewrite.
Explain why CommonJS output defeats tree shaking, when to set commonjs: true, and why internal dist paths must never appear in source.
Decide which tools in the pipeline own the alias (bundler, tests, server rendering) so every environment sees the same module graph.
## Two places to alias There are two moments in a web build where `react-native` can be swapped for `react-native-web`: | Where | How | Sees it | |---|---|---| | **Module resolution** (bundler) | webpack `resolve.alias` with `'react-native$'` | only that bundler | | **Compilation** (Babel) | a Babel plugin rewrites the import string | every tool that runs the same Babel config | A Babel alias is useful when the bundler has no alias feature you control, or when several tools (a bundler, a test runner, a server-rendering build) share one Babel configuration. The trade-off is that it only covers files Babel actually processes: a dependency excluded from Babel still contains the original `react-native` import. ## The module-resolver alias React Native Web's setup guide shows `babel-plugin-module-resolver` with an anchored pattern: ```json { "plugins": [ ["module-resolver", { "alias": { "^react-native$": "react-native-web" } }] ] } ``` The `^` and `$` anchors play the same role as webpack's trailing `$`: only the exact specifier is replaced, so deep `react-native/...` imports are left alone. ## What babel-plugin-react-native-web adds React Native Web ships its own Babel plugin, `babel-plugin-react-native-web`, enabled with `plugins: ['react-native-web']`. It does two things: 1. **It aliases.** Imports, `require` calls and re-exports of `react-native` (or `react-native-web`) are redirected to `react-native-web`. 2. **It splits imports per export.** For every named import it knows, it emits a default import from that export's own module: ```javascript // before import { StyleSheet, View } from 'react-native'; // after import StyleSheet from 'react-native-web/dist/exports/StyleSheet'; import View from 'react-native-web/dist/exports/View'; ``` A name the plugin does not know (for example an Android-only API that React Native Web does not implement) is imported from the package index instead, where it resolves to `undefined`. ## Why the per-export rewrite matters - React Native's Babel preset converts ES modules to **CommonJS**. A bundler cannot tree-shake a CommonJS index, so `import { View } from 'react-native'` would otherwise pull in every component React Native Web exports. - With per-export paths, the bundle contains only the modules the app actually imports, which is the "prune modules not used by your application" step the React Native Web docs recommend. ## The commonjs option The plugin writes paths into `react-native-web/dist/exports/...` by default, which is the ES-module build. If the bundler consumes CommonJS, set the option: ```json { "plugins": [["react-native-web", { "commonjs": true }]] } ``` and the paths become `react-native-web/dist/cjs/exports/...`. Match the option to the module format your bundler actually consumes. ## Rules that keep it safe - **Never write the `dist/exports/...` paths by hand.** React Native Web's README says the internal paths are not stable; only the plugin should produce them. - **Use one alias mechanism per tool.** A Babel alias plus a bundler alias is harmless but confusing; pick one and document it. - **Other tools need their own mapping** unless they run the same Babel config: React Native Web's docs show Jest's `moduleNameMapper` with `'^react-native$'` and a Node module-alias setup for server rendering. - **Expo projects need none of this**; Expo's Metro configuration performs the alias for the web platform. ## How to check that pruning works 1. Build the web bundle for production with the plugin enabled. 2. Open a bundle analyser (or the bundler's stats) and look for React Native Web modules the app never imports, such as `VirtualizedList` in an app without lists. 3. If the whole library is present, look for an import the plugin could not rewrite: a namespace import (`import * as RN from 'react-native'`), a dependency that Babel does not process, or a mismatch between the `commonjs` option and the build being consumed. 4. Rebuild and compare; the difference is the saving the per-export rewrite provides. ## Choosing between them For a webpack-based web target, the usual setup is both: the webpack alias for resolution and `babel-plugin-react-native-web` for pruning. For a toolchain without a configurable resolver, the Babel plugin alone does both jobs.
- What does babel-plugin-react-native-web do with import { ToastAndroid } from 'react-native'?It does not know `ToastAndroid` as a React Native Web export, so it rewrites the import to come from the package index instead of a per-export path. React Native Web's index does not export it either, so the value is `undefined` at runtime and the first call throws a `TypeError`.
- Why is the output of babel-plugin-react-native-web not something you should copy into source files?Because the `react-native-web/dist/exports/...` paths are internal and, per the plugin's README, not stable. A library upgrade can move them. Source code should always import from `react-native` and let the plugin produce the internal paths at build time.
saying these in an interview costs you the question
- A Babel alias also covers dependencies that Babel never processes.
- Named imports from react-native tree-shake fine even after conversion to CommonJS.
- You should import react-native-web/dist/exports/View directly for smaller bundles.
- babel-plugin-react-native-web only optimises; you still need a separate alias.
- The commonjs option makes the plugin output ES modules.