In a React Native app bundled by Metro, how do you import SVG icon files as components, and why does babelTransformerPath matter?
answer
- svg is in Metro's default assetExts
- core has no SVG drawing component
- react-native-svg draws, a transformer converts
- babelTransformerPath plus move svg to sourceExts
- delegate other files to the upstream transformer
basics
~20 sMetro treats .svg as an image asset by default, and core React Native cannot draw SVG. Point transformer.babelTransformerPath at an SVG transformer that turns each .svg into a react-native-svg component, and move svg from assetExts to sourceExts.
solid answer
~40 sBy default `svg` is in Metro's `assetExts`, so `import Logo from './logo.svg'` gives you an asset reference, not something React Native can draw: core has no SVG component, and `react-native-svg` provides the drawing primitives. To import icons as components you install `react-native-svg` plus an SVG transformer (the community package is `react-native-svg-transformer`), set `transformer.babelTransformerPath` to it, and move `svg` from `assetExts` to `sourceExts` so the file is transformed as code. The transformer converts SVG markup into a component module and hands every other file to the normal Babel transformer. In Expo, `getDefaultConfig` already sets its own transformer, so the replacement must delegate to `@expo/metro-config/babel-transformer`. Add a `declare module '*.svg'` type so TypeScript accepts the import, and restart Metro with a clean cache.
code
javascript · 15 lines// metro.config.js (bare React Native 0.87)
const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
const defaultConfig = getDefaultConfig(__dirname);
const {assetExts, sourceExts} = defaultConfig.resolver;
module.exports = mergeConfig(defaultConfig, {
transformer: {
babelTransformerPath: require.resolve('react-native-svg-transformer'),
},
resolver: {
assetExts: assetExts.filter((ext) => ext !== 'svg'),
sourceExts: [...sourceExts, 'svg'],
},
});go deeper
Recall that SVG needs react-native-svg to draw and that Metro treats .svg as an image asset until configured otherwise.
Explain the full wiring: babelTransformerPath, moving svg between assetExts and sourceExts, the type declaration and a clean restart.
Keep the transformer chain correct in Expo by delegating to Expo's transformer, and debug imports that still resolve as assets.
Choose between a build-time transformer and pre-converted components by weighing icon churn, upgrade risk and how much custom Metro config the team will own.
## Why an SVG import does not just work A design team hands over icons as `.svg` files and wants to use them like components: `<Logo width={24} height={24} />`. Out of the box that fails for two separate reasons. 1. **Metro classifies `.svg` as an asset.** `svg` sits in Metro's default `resolver.assetExts` list next to `png` and `jpg`. An asset import is not compiled; it becomes a reference to a file copied into the app, which `Image` can load by source. 2. **React Native core cannot draw SVG.** There is no vector-drawing component in `react-native`. The `react-native-svg` library supplies `Svg`, `Path`, `Circle`, `Rect` and the other primitives as native views. So a working setup needs both a renderer (`react-native-svg`) and a build step that turns SVG markup into a component made of those primitives. ## Where babelTransformerPath comes in Metro's transform worker compiles each source file by calling the module named in **`transformer.babelTransformerPath`**. React Native points it at `@react-native/metro-babel-transformer`; Expo points it at its own. That makes it the natural hook for a custom file type: a replacement transformer receives `{ src, filename, options }` for every file, and can do something different for one extension. An SVG transformer does exactly this: - if `filename` ends in `.svg`, it converts the markup into JavaScript source for a component built from `react-native-svg` primitives, then compiles that source with Babel; - for every other file, it **delegates to the upstream transformer**, so JSX, TypeScript and your Babel config keep working. The community package for this is `react-native-svg-transformer`. ## The configuration, step by step 1. Install `react-native-svg` (it has native code, so a bare app or development build must be rebuilt) and the SVG transformer package. 2. In `metro.config.js`, set `transformer.babelTransformerPath` to the transformer. 3. Remove `svg` from `resolver.assetExts` and add it to `resolver.sourceExts`, so Metro sends `.svg` files to the transformer instead of copying them as assets. 4. Add a TypeScript declaration so `import Logo from './logo.svg'` type-checks. 5. Restart Metro with a clean cache: the transform configuration changed, and cached output for existing files must not be reused. Step 3 is the one people forget. With `babelTransformerPath` changed but `svg` still in `assetExts`, the transformer is never called for `.svg` files and the import still yields an asset reference. ## The Expo wrinkle `getDefaultConfig` from `expo/metro-config` already sets `babelTransformerPath` to `@expo/metro-config/babel-transformer`, so that `babel-preset-expo` and its web and server handling always apply. Overwriting the key with a transformer that delegates to the bare React Native transformer silently drops Expo's. Expo's documentation says to extend its transformer instead: a custom transformer handles `.svg` and passes everything else to `require('@expo/metro-config/babel-transformer').transform(...)`. ## Alternatives and tradeoffs | Approach | How it works | Cost | |---|---|---| | Transformer | `.svg` becomes a component at build time | Metro config to maintain; must match your framework's upstream transformer | | Pre-converted components | Convert SVGs to `.tsx` once (the Expo docs point to SVGR) and commit them | No build hook, but regenerate on every design change | | Raster assets | Export PNGs at `@2x`/`@3x` | No vector scaling or runtime colour changes | The transformer wins when icons change often and designers own the files. Pre-converted components win when the icon set is small and stable, or when a team wants no custom Metro configuration. ## Common failure modes - Icons render as nothing or an import error appears: `svg` is still an asset extension. - Every JSX file breaks after the change: the custom transformer does not delegate non-SVG files. - Expo-specific behaviour disappears: the replacement delegates to the React Native transformer instead of Expo's. - It works after a fix only on some machines: Metro was not restarted with a clean cache. - Tests fail on the new imports: Jest does not use Metro's transformer, so `.svg` files need their own mapping in the Jest config. ## What interviewers listen for A good answer separates the two problems (Metro's asset classification and the missing renderer), names `babelTransformerPath` as the hook, remembers the extension lists, and knows that in Expo the custom transformer has to chain to Expo's rather than replace it.
- You set babelTransformerPath but the SVG import still returns an asset reference. Why?`svg` is still in `resolver.assetExts`, so Metro resolves the file as an asset and never sends it to the transformer. Remove it from `assetExts`, add it to `sourceExts`, and restart Metro with a clean cache.
- In an Expo project, why not simply overwrite babelTransformerPath with the SVG transformer?Expo's `getDefaultConfig` already sets it to `@expo/metro-config/babel-transformer`, which keeps `babel-preset-expo` and its web and server handling in place. A replacement must pass non-SVG files to that Expo transformer, not to the bare React Native one, or those Expo transforms are lost.
saying these in an interview costs you the question
- React Native's Image component renders SVG icons as vectors on its own
- Setting babelTransformerPath alone is enough; the extension lists can stay
- A custom Babel transformer only needs to handle .svg files
- In Expo you can overwrite babelTransformerPath without delegating to Expo's
- SVG support is a pure JavaScript change with no native rebuild