skip to content

In Metro, what does the inlineRequires option returned by getTransformOptions do, and how can enabling it break a module that relies on side effects?

level: seniorimportance: should knowfreq 25%

answer

  1. require moved to its first use
  2. RN default true, Expo default false
  3. evaluation order changes, some never run
  4. blockList per file, nonInlinedRequires per specifier
  5. options vary by dev and platform

basics

~20 s

inlineRequires makes Metro move module-level require() bindings to where they are used, so a module is evaluated on first use instead of at load. A module whose side effects others depend on can then run late, out of order, or never.

solid answer

~50 s

`getTransformOptions` in the Metro config returns per-bundle transform options; `transform.inlineRequires` turns on a pass that rewrites `const Foo = require('./Foo')` so that each use becomes `require('./Foo')` inline. The module is then evaluated the first time code touches it rather than when the requiring file loads. `@react-native/metro-config` returns `inlineRequires: true`; Expo's config returns `false`. The React Native docs warn that inlining changes evaluation order and can mean a module is never evaluated. So a file that is required only for its side effects, such as installing a logger, a polyfill-like global patch or a handler, may run after the code that needed it. Fixes: keep that `require` out of inlining with `nonInlinedRequires` (by specifier), exclude a whole file with `inlineRequires: { blockList: {...} }`, make the side effect an explicit call at startup, or turn the option off.

code

javascript · 20 lines
javascript
// metro.config.js (bare React Native 0.87)
const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');

module.exports = mergeConfig(getDefaultConfig(__dirname), {
  transformer: {
    async getTransformOptions() {
      return {
        transform: {
          experimentalImportSupport: false,
          inlineRequires: {
            // Every require() in the entry file stays eager.
            blockList: {[require.resolve('./index.js')]: true},
          },
          // require('./src/logger') is never inlined anywhere.
          nonInlinedRequires: ['./src/logger', 'react'],
        },
      };
    },
  },
});

go deeper

for a junior

Recall that inline requires delay evaluating a module until its first use, and that this is configured in Metro's getTransformOptions.

for a middle

Explain the rewrite of module-level require bindings and the different defaults of @react-native/metro-config and expo/metro-config.

for a senior

Diagnose side-effect modules that run late or never after inlining, and fix them narrowly with nonInlinedRequires, blockList or an explicit install call.

for a principal

Set a team rule that modules must be side-effect free or expose explicit initialisation, so the optimisation stays safe as the codebase grows.

## What the option is Metro's config has a **`transformer.getTransformOptions`** function. Metro calls it for each bundle it builds, passing the entry points and an options object with `dev` and `platform`, and uses the returned object to tune the transformer. Its `transform` field carries two keys that matter here: - **`inlineRequires`**: `true`, `false`, or `{ blockList: { [absolutePath]: true } }`; - **`nonInlinedRequires`**: module specifiers such as `'react'` whose `require` calls are never inlined. Defaults differ by framework: | Config | `inlineRequires` | |---|---| | Metro's own defaults | `false` | | `@react-native/metro-config` (React Native CLI apps) | `true` | | `expo/metro-config` | `false` | ## What the transform does With the option on, Metro runs its **inline-requires** Babel pass on each file. It finds module-level bindings created from a `require()` call and replaces every use of the binding with the call itself: ```js // before const analytics = require('./analytics'); export function track(e) { analytics.send(e); } // after export function track(e) { require('./analytics').send(e); } ``` Metro's module system evaluates a module the first time `require` is called for it and caches the result, so the module now runs **on first use** rather than when the requiring file loads. The React Native docs describe this as applying to `require` calls, not `import` declarations; when Metro compiles `import`s itself (`experimentalImportSupport`, Expo's default) its import helpers are treated as inlineable too. ## How it breaks side effects The React Native 0.87 docs are explicit: inlining **changes the order in which modules are evaluated, and can cause some modules never to be evaluated**. That is harmless for side-effect-free modules and harmful for the others. Typical breakages: 1. **A global patched too late.** `const logger = require('./logger')` in the entry file, where evaluating `logger` wraps `console` or installs an error handler. If `logger` is only referenced inside a function, the patch happens when that function first runs, after errors it should have caught. 2. **A registration that never happens.** A module that registers itself (a background task, a custom handler) and is bound but never used is never evaluated once its binding is inlined away. 3. **An order dependency flipped.** File A requires B, which sets something up, then C, which reads it. With inlining, C can be evaluated at its first use, before B. The symptom is usually confusing: the code works in a debugging session where something happened to touch the module early, and fails elsewhere. ## The fixes, from narrow to broad 1. **Exclude the specifier**: add it to `nonInlinedRequires`, for example `['./logger']` or `['react']`; the `require` stays where it is everywhere. 2. **Exclude a file**: `inlineRequires: { blockList: { [require.resolve('./index.js')]: true } }` keeps every `require` in that file eager while inlining elsewhere. 3. **Make the side effect explicit**: export an `install()` function and call it at startup, so correctness does not depend on evaluation timing. 4. **Turn the option off** for the app, accepting the startup cost. Because `getTransformOptions` receives `dev` and `platform`, a team can also return different values per build, although differing development and production behaviour makes these bugs harder to reproduce. ## Operational notes - Restart Metro after changing transform options, so every file is transformed again with the new settings. - Keep the escape hatches documented next to the config: a `nonInlinedRequires` entry with no comment is the next person's mystery. - Treat inlining as correct only for side-effect-free modules; the fix belongs in the module that has the side effect. - When a bug appears only in one kind of build, compare what `getTransformOptions` returns for that `dev` and `platform` combination before looking anywhere else. - A bare `require('./setup')` with no binding has nothing to inline, so it stays eager; the risk is a module that is bound to a name and then used only later. ## What a senior answer shows It explains the rewrite, knows the defaults differ between the React Native CLI and Expo, predicts the failure mode from evaluation order, and reaches for the narrowest fix (`nonInlinedRequires` or `blockList`) before switching the whole optimisation off.

  • What is the difference between inlineRequires.blockList and nonInlinedRequires in Metro?
    `blockList` is keyed by absolute file path and disables inlining for every `require` inside that file. `nonInlinedRequires` lists unresolved specifiers such as `'react'` and keeps those particular `require` calls un-inlined in every file.
  • An Expo app and a React Native CLI app behave differently with the same side-effect module. Why might that be?
    Their Metro defaults differ: `@react-native/metro-config` returns `inlineRequires: true`, while `expo/metro-config` returns `false`. The same module is evaluated eagerly in the Expo app and lazily in the CLI app unless either config overrides the default.

saying these in an interview costs you the question

  • Inline requires only change bundle size, never runtime behaviour
  • Expo apps inline requires by default, just like CLI apps
  • blockList takes module names such as 'react', not file paths
  • A module bound with require is always evaluated even if unused
  • The only fix for a side-effect bug is disabling inlining entirely