esbuild & react-native-web - IV: vector-icons
Tags: reactnative
Published: 2021-03-13 21:43:01

The [react-native-vector-icons](https://github.com/oblador/react-native-vector-icons) (`rnvi`) is a popular library in the react-native world, which is compatible with react-native-web.

But there is a problem. When used with react-native-web, `rnvi` significantly increases the bundle size. Each `ttf` font has a corresponding GlyphMap, which is a `json` file that associates the icon's name with its Unicode value. Even if you use only one icon in your project, the entire GhyphMap and the entire `ttf` will be imported by esbuild during the bundling. Some GlyphMaps, such as Ionicons and MaterialCommunityIcons, are over 100 kb, and minimization has almost no effect. And the `MaterialCommunityIcons.ttf`, which is more than 900 kb, will be download into your app.

It can get even worse, because popular libraries as [react-native-paper](https://callstack.github.io/react-native-paper/icons.html) and [@react-navigation/material-bottom-tabs](https://reactnavigation.org/docs/material-bottom-tab-navigator/) depend on `rnvi` for the _MaterialCommunityIcons_ font. You can opt-out `rnvi` using `babel-plugin-optional-require`, but, as you can infer, it doesn't work with esbuild. When you install one of these libraries in a react-native-web project, esbuild will ask you to install `rnvi` and will bundle the large GlyphMap for the MaterialCommunityIcons, even if you haven't used any icons in your project. 

In this post, we will discuss some options to solve this problem. 

## Expo

Before we discuss the options, let me give you a suggestion. Use [`@expo/vector-icons`](https://docs.expo.io/guides/icons/) instead of `react-native-vector-icons`, because `@expo/vector-icons` is distributed with ES modules, that allow tree-shaking and it will configure the icon font in your webpage automatically for you:
```html
<html>
<head>
<style id="expo-generated-fonts" type="text/css">
@font-face {
    font-family: material-community;
    src: url(/assets/MaterialCommunityIcons-QCSDDVWU.ttf);
    font-display: auto;
  }
</style>
...
```
`@expo/vector-icons` allow the use of customized fonts or subsets of fonts created with [Fontello](https://fontello.com/) or [Icomoon](https://icomoon.io/). Unfortunately, these sites don't work with MaterialCommunityIcons.

Behind the scenes, `@expo/vector-icons` uses the library [`expo-font`](https://github.com/expo/expo/tree/master/packages/expo-font) to do it. You can use these Expo libraries even out of a Expo project.

To use `@expo/vector-icons` with react-native-web and esbuild, configure your `tsconfig.json/jsconfig.json`:


```json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "react-native": ["./node_modules/react-native-web"],
      "react-native-vector-icons/MaterialCommunityIcons": [
         "./node_modules/@expo/vector-icons/MaterialCommunityIcons.js"
      ]
    }
  }
}
```
You need to specify one alias for each icon font that you use.

## Solutions

#### Removing MaterialCommunityIcons when you are not using any Icon with react-native-paper or react-navigation
If you are using `react-native-paper` or `@react-navigation/material-bottom-tabs` and you are not using any icon from MaterialCommunityIcons, you can remove it with the following plugin:
```javascript
require('esbuild')
  .build({
    ...
    plugins: [{
        name: 'remove-materialicons',
        setup(build) {
          build.onLoad({ filter: /MaterialCommunityIcons.js/ }, () => ({
            loader: 'jsx',
            contents: `  
                import * as React from 'react';
                import { Text } from 'react-native';
                export default ({ name, color, size, ...rest }) =>  (
                  <Text {...rest} 
                    style={{backgroundColor: 'transparent', color, fontSize: size }}
                    pointerEvents="none"
                    selectable={false}
                  >□</Text>
                );
              `,
          }))
        },
      },
    ]
  })
```
For it to work you need to alias `rnvi` to `@expo/vector-icons`, according to the `jsconfig.json/tsconfig.json` above.

#### Using material-icons-subset

The best solution is to use the package [`material-icons-subset`](https://www.npmjs.com/package/material-icons-subset) to create a subset of the MaterialCommunityIcons font. 

`npm i material-icons-subset` 

Pass a list of icon names to be included in the font as arguments in the command line:
```
material-icons-subset camera menu account-outline email archive 
```

Or, you can pass the path for a config.json file:
```
material-icons-subset font-config.json 
```

The config.json file must have an array called `icons` with the name of the icons to be included in the font:

```JSON
{
   "icons": [
      "camera",
      "menu",
      "account-outline",
      "tune",
      "bookmark-outline",
      "pause",
      "arrow-left",
      "archive",
      "email"]
}
```
The library will create two files:
 `materialdesignicons-webfont.ttf` and 
 `materialdesignicons-webfont.json` with the GlyphMap.

You need to alias `MaterialCommunityIcons.ttf` and `MaterialCommunityIcons.json` files to the respectively new files that you created with material-icons-subset. 
Unfortunately, I couldn make it work in the `jsconfig.json/tsconfig.json`. 
Therefore, you need to use a esbuild plugin to make it work:

```javascript
const materialIconsPlugin = {
  name: 'material-icons',
  setup(build) {
    build.onResolve({ filter: /MaterialCommunityIcons\.(ttf|json)/ }, (args) => ({
      path: resolve(`./src/assets/materialdesignicons-webfont${parse(args.path).ext}`),
    }))
  },
}

```

## Conclusion

That's it. Now you can use `vector-icons` with react-native-web and esbuild without compromising the bundle size of your app.