skip to content

In a webpack build of a React Native codebase, how do you alias react-native to react-native-web, and why 'react-native$' and .web.js first?

level: middleimportance: must knowfreq 38%

answer

  1. one resolve.alias entry
  2. the dollar sign means exact match
  3. deep imports escape the alias
  4. first extension that matches wins
  5. some React Native packages ship untranspiled

basics

~10 s

Add resolve.alias { 'react-native$': 'react-native-web' } so only the exact react-native import is swapped, and list '.web.js' before '.js' in resolve.extensions so web-specific files win over shared ones.

solid answer

~40 s

`react-native-web` is installed next to `react-dom`, and webpack is told to resolve the package name to it: `resolve: { alias: { 'react-native$': 'react-native-web' } }`. The trailing `$` makes it an **exact match**, so only `import … from 'react-native'` is redirected; a deep import such as `react-native/Libraries/…` is *not* aliased and pulls React Native's own native-only source into the web build, which then fails (deep imports are also a type error under React Native 0.87's Strict TypeScript API). `resolve.extensions` lists `.web.js` before `.js` (and `.web.tsx` before `.tsx`) because webpack takes the **first extension that exists**: that order is what makes `Button.web.js` replace `Button.js` in the web bundle. Many React Native libraries publish untranspiled JSX or Flow, so their `node_modules` folders also need to go through `babel-loader`.

code

javascript · 29 lines
javascript
// web/webpack.config.js (fragment)
const path = require('path');
const appDirectory = path.resolve(__dirname, '../');

module.exports = {
  entry: path.resolve(appDirectory, 'index.web.js'),
  module: {
    rules: [
      {
        test: /\.[jt]sx?$/,
        include: [
          path.resolve(appDirectory, 'src'),
          path.resolve(appDirectory, 'node_modules/some-untranspiled-rn-lib'),
        ],
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['module:@react-native/babel-preset'],
            plugins: ['react-native-web'],
          },
        },
      },
    ],
  },
  resolve: {
    alias: { 'react-native$': 'react-native-web' },
    extensions: ['.web.tsx', '.web.ts', '.tsx', '.ts', '.web.js', '.js'],
  },
};

go deeper

for a junior

Recall that a web build maps the react-native import to react-native-web in the bundler, and that react-dom must be installed alongside it.

for a middle

Explain the exact-match alias, why deep imports escape it, and how the extension order makes .web files override shared ones.

for a senior

Cover the practical failures: untranspiled dependencies, deep imports in libraries, and a web-first extension list that includes the TypeScript variants.

for a principal

Judge whether a hand-rolled webpack setup is worth maintaining against a framework that owns the alias and resolution for every platform.

## What the alias does A React Native codebase imports its primitives from one package: `import { View, Text } from 'react-native'`. **React Native Web** (`react-native-web`, 0.21 here) exports the same names implemented with React DOM. A web build therefore does not change the application code; it changes **module resolution**, so the bundler hands the web implementation to every file that asks for `react-native`. With webpack the documented setup is a single alias: ```javascript // webpack.config.js module.exports = { resolve: { alias: { 'react-native$': 'react-native-web' }, extensions: ['.web.tsx', '.web.ts', '.tsx', '.ts', '.web.js', '.js'] } }; ``` `react-dom` must be installed too, because React Native Web renders through it. ## Why the key ends in a dollar sign In webpack's alias syntax, a trailing `$` means **exact match**. Only the bare specifier `react-native` is replaced: | Import in source | Resolves to | |---|---| | `from 'react-native'` | `react-native-web` | | `from 'react-native/Libraries/Utilities/codegenNativeComponent'` | React Native's own file, not aliased | Without the `$`, webpack would rewrite deep paths into `react-native-web/Libraries/...`, which do not exist, and the build would fail with confusing errors. With it, deep imports reach React Native's native source (Flow types, native module lookups) and fail anyway. The real fix is to have **no deep imports**: in React Native 0.87 they are already a type error under the Strict TypeScript API, and React Native Web cannot serve them. ## Why .web.js comes first `resolve.extensions` is an ordered list, and the resolver takes the **first file that exists**: 1. `import Button from './Button'` looks for `Button.web.tsx`, `Button.web.ts`, then `Button.tsx`, and so on. 2. If `Button.web.tsx` exists, it wins; the native file is never read. 3. Reverse the order and the shared `Button.tsx` would win every time, silently ignoring every web override. The same order must also include `.ts` and `.tsx` for a TypeScript codebase, each with its `.web.` variant first. ## The untranspiled-package problem React Native projects are normally bundled by Metro, which transpiles `node_modules` with React Native's Babel preset. Webpack setups usually skip `node_modules`. Many React Native libraries publish **untranspiled source** (JSX, Flow annotations, modern syntax), so a web build must: - include those specific packages in `babel-loader`'s `include` list; - use `@react-native/babel-preset` (the preset the React Native Web docs recommend) so the syntax matches what Metro would accept; - add the `react-native-web` Babel plugin for pruning unused exports. ## Diagnosing a broken alias | Symptom in the web build | Usual cause | |---|---| | Syntax error inside a `node_modules` React Native package | the package ships untranspiled JSX or Flow and is not in the loader's `include` list | | "Module not found" for a `react-native/Libraries/...` path | a deep import, which the exact-match alias deliberately leaves alone | | The web shows the native variant of a component | `.js` or `.tsx` is listed before its `.web.` counterpart | | Two copies of React Native Web in the bundle | a library depends on a different `react-native-web` version; dedupe it | | `undefined` is not a component at runtime | the import names an API React Native Web does not export | Reading the resolved path of the failing module (webpack's stats or the error's import trace) usually points straight at one of these rows. ## Checklist for a first web build - `react-dom` and `react-native-web` installed at versions matching the app's React. - `'react-native$'` alias and web-first extension order. - `babel-loader` covering app source and untranspiled dependencies. - An image loader for static assets imported with `require`. - A web entry (for example `index.web.js`) that registers the app with `AppRegistry` and runs it into a DOM element. - HTML root styles that give the app full height, since a root `View` with `flex: 1` needs a sized parent. ## Where this sits among the alternatives The same idea appears in every toolchain: Babel's module-resolver can do the alias at compile time, Jest can map it with `moduleNameMapper`, Node can do it with a module-alias package for server rendering, and Expo does it inside its Metro configuration so an Expo app needs no alias at all.

  • With webpack aliasing 'react-native$' to react-native-web, what happens to import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'?
    It is not aliased, because the `$` restricts the alias to the exact specifier. Webpack resolves React Native's own file, which is Flow-typed, native-only code, so the web build fails or the module breaks at runtime. The fix is to keep such imports in native-only files (`.native.tsx`) so the web bundle never reaches them.
  • In a webpack setup for React Native Web, why must some node_modules packages be passed through babel-loader?
    Because many React Native libraries publish untranspiled JSX, Flow or modern syntax, relying on Metro to transpile them. Webpack configurations usually exclude `node_modules`, so those packages fail to parse. Add each one to the loader's `include` list and use `@react-native/babel-preset` so it is compiled as Metro would.

saying these in an interview costs you the question

  • Without the $ the alias is safer because it also covers deep imports.
  • The order of resolve.extensions does not matter; webpack picks the most specific file.
  • The alias requires changing every import to react-native-web in source.
  • Deep imports from react-native/Libraries work on the web once the alias is set.
  • react-dom is optional because react-native-web renders by itself.