ES

Navigate back to the homepage

Building a maintainable Icon System for React and React Native

Ema Suriano
October 1st, 2019 · 5 min read

Implementing a maintainable icon system for a React and React Native project can be a hard task, especially when it comes to achieving the same workflow to add/remove/use an icon in all the platform (Web, Android, and iOS). In this post, I will share how we implemented a consistent icon system inside our component library at Omio.

The problematic 😞

If you come from a web background, it’s well known that you can use the <svg> to directly render an SVG image into the DOM. This is perfect because browsers are prepared for that.

Sadly, this is not the same scenario for React Native … First of all, the platform doesn’t support rendering SVG directly and it seems this is going to stay like this for a while. You can follow this feature request for more information.

Therefore the community has created a few workarounds using 3rd party libraries outside React Native to deal with SVG. Some examples are: react-native-svg, react-native-svg-uri, react-native-svg-asset-plugin.

In summary, you can’t use the same renderer in both platforms (web and mobile), and also depending on the React Native library you picked the API of your component will change. This can a problem when working with projects written in React and React Native where the main objective is to share as much code as possible. Therefore you need to look for a way to abstract all these implementation details.

Proposed solution: Auto-generated Icons 👍

Let’s set the context that you start with this kind of folder structure:

1assets/
2 bus.svg
3 train.svg
4 flight.svg
5 ferry.svg
6
7components
8 Icon.js
9 Icon.native.js

You have all your in SVG icons inside an /assets folder. The normal workflow will be to create one component for web and another for mobile, using some abstraction for each one like Icon.js and Icon.native.js.

But what if you can automate this process, so the only task you need to do is adding/removing icons from this assets folder. This is when generating icons becomes quite helpful!

1assets/
2 bus.svg
3 train.svg
4 flight.svg
5 ferry.svg
6
7** cast magic spell **
8 # some idea of what the generated folder will look like here

The generation of icon components occurs right before starting and building the application; this is to ensure that they are always up to date. The result will be a file which exports all the icons as React Components, each of which will call the proper implementation in the respective platform.

At the same time, both platforms should implement the same API (propTypes) making the icon system consistent and proving a better development experience.

Demo time 🎉

Final Demo
Final Demo

I created a repository from which I extracted the following snippets. If you want to skip the explanation and jump into the code, you can use this link:

Time to code 👨‍💻

Let's start!
Let's start!

So let’s start a clean project using create-react-app and create-react-native-app. After playing around a bit I realized that it’s easier to bootstrap the project using create-react-native-app and then add the missing files to the project.

1> create-react-native-app MaintainableIconSystemReact
2> create-react-app delete-me-later

Then inside the folder, you need to add the /public folder with the delete-me-later project and also create the assets folder that will hold the icons.

Download the icons you want to use in your project (make sure all of them are SVG) and place them inside the /assets folder.

One small reminder in case you want to use another set of icons: double-check that the icons are using the property fill to set the color of it and not stroke. There are some workarounds to convert stroke to fill, but I’m not going to cover those in this post.

Generating Icon Components

This will be the entry point for both platforms. Each SVG icon inside the assets folder will be transformed into a React component which then will call the platform-specific implementation (next two sections).

In summary this step should:

  • Read the icons from the assets folder.
  • For each icon create a React Component with the proper name and send the name of the original icon.
  • Export as module the generated icons.
1const { readdirSync } = require('fs');
2
3// some helpful functions
4const isSVG = file => /.svg$/.test(file);
5const removeExtension = file => file.split('.')[0];
6const toPascalCase = string =>
7 string
8 .match(/[a-z]+/gi)
9 .map(word => word.charAt(0).toUpperCase() + word.substr(1).toLowerCase())
10 .join('');
11
12// getting all the icons
13const icons = readdirSync(ICON_SOURCE_FOLDER)
14 .filter(isSVG)
15 .map(removeExtension);
16
17const indexContent = [
18 "import React from 'react';",
19 "import Icon from './Icon';",
20 '',
21 icons
22 .map(
23 icon =>
24 `export const ${toPascalCase(
25 icon,
26 )} = props => <Icon {...props} name="${icon}" />;`,
27 )
28 .join('\n'),
29].join('\n');
30
31writeFileSync(`src/components/Icon/index.js`, indexContent);
32console.log('Icon component file created! ✅');

The result of this script should be an index.js file located inside src/components/Icon which should look similar to:

1import React from 'react';
2import Icon from './Icon';
3
4export const Louvre = props => <Icon {...props} name="001-louvre" />;
5export const LeaningTowerOfPisa = props => (
6 <Icon {...props} name="002-leaning tower of pisa" />
7);
8export const Coliseum = props => <Icon {...props} name="003-coliseum" />;
9// and the rest of the icons

Implementing Web Abstraction 🖥

As I said in the introduction, the web is already prepared to render SVG; the only thing you need to solve is how your project will load them. Otherwise, when you try to run your project it will throw an exception because it doesn’t know how to handle this type of file.

As this POC is based on create-react-app it has already integrated an SVG loader inside its hidden Webpack configuration. The way it works is by exporting a ReactComponent in the import of the SVG, which will display the proper icon. For example:

1import { ReactComponent } from './my-awesome-icon.svg';
2
3const MyApp = () => (
4 <div>
5 <p>This is my awesome icon!</p>
6 <ReactComponent />
7 </div>
8);

So for this step, the only task you need to do is to aggregate all the icons inside a map which then can be used in the Icon.js component. This is the corresponding snippet:

1const iconMapContent = [
2 icons
3 .map(
4 icon =>
5 `import { ReactComponent as ${toPascalCase(
6 icon,
7 )} } from './${icon}.svg';`,
8 )
9 .join('\n'),
10 '',
11 'export default {',
12 icons.map(icon => `"${icon}": ${toPascalCase(icon)}, `).join('\n'),
13 '};',
14].join('\n');
15
16writeFileSync(`src/assets/icons/icon-map.js`, iconMapContent);
17console.log('Web Icon Map created! ✅');

The result of it will be a file called icon-map.js inside the assets/icons folder with all the special import to get the React Component from the svg and then exports all of them in a map with key as the name of the originalFile.

1import { ReactComponent as AddCircle } from './add_circle.svg';
2import { ReactComponent as Alarm } from './alarm.svg';
3import { ReactComponent as Assistant } from './assistant.svg';
4
5// and the list continues ...
6
7export default {
8 add_circle: AddCircle,
9 alarm: Alarm,
10 assistant: Assistant,
11};

The last thing to do is to create the Icon component for Web. The idea behind this component is to maintain the props between Web and Native. This will make the use of the component platform agnostic, saving a lot of time when developing.

For this example the shared props between platform are:

  • name: the name of the file of the icon. This prop is specified by the Icon/index.js when you import a specific icon
  • size: how big it will be. Here you can also set the default size
  • color: the general colour of the icon; by default will be black
1import React from 'react';
2import iconMap from 'assets/icons/icon-map';
3
4const Icon = ({ name, size, color, ...rest }) => {
5 const Icon = iconMap[name];
6 return <Icon color={color} style={{ width: size, height: size }} {...rest} />;
7};
8
9Icon.propTypes = {
10 name: PropTypes.string.isRequired,
11 size: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
12 color: PropTypes.string,
13};
14
15Icon.defaultProps = {
16 size: '5em',
17 color: 'black',
18};
19
20export default Icon;

Implementing Native Abstraction 📱

One of the most performant approaches inside the React Native world is treating your icons as a custom font, and then when you need to render an icon it will just be a Text tag with a special character and using this custom font. So the order of steps will be:

  1. Generate the custom font with the map of characters.
  2. Load it inside our application.
  3. Create Icon.native.js.

In order to group all the icons inside a single font file you should install icon-font-generator which given a path it will generate:

  • The .ttf file with all the fonts included
  • The Glyph Map, which has keys as the name of the icon and values equals to the character/position of it inside the font

There is a little fix you need to apply for the generated Glyph Map because the values of each icon are expressed in Hexa and React Native can’t read them. The solution for this is to parse each value to decimal.

1execSync(
2 `icon-font-generator ./src/assets/icons/*.svg -o ./src/assets/fonts -n custom-font-icon -c false --html false --types ttf --height 500`,
3);
4
5const glyphMap = JSON.parse(
6 readFileSync(`./src/assets/fonts/custom-font-icon.json`),
7);
8
9const customFontContent = [
10 '{',
11 icons
12 .map(value => `"${value}": ${parseInt(glyphMap[value].substr(1), 16)}`)
13 .join(',\n'),
14 '}',
15].join('\n');
16
17writeFileSync(`./src/assets/fonts/custom-font-icon.json`, customFontContent);
18console.log('React Native Asset generated! ✅');

The output of this script will generate the already mentioned files inside the folder assets/fonts. The next step is to load it inside your application.

If you are using a project with create-react-native-app, you need to set the folder of assets/fonts as a resource folder for the native projects. To do that, add the following property to your package.json:

1{
2 "rnpm": {
3 "assets": ["./src/assets/fonts/"]
4 }
5}

And then execute the command ”react-native link” inside the root of your project, which will change the configuration of Android and iOS project and load the font when the application starts.

The last step of this implementation is creating the Icon.native.js which has to make use of the generated font and render the proper icon. For that, I suggest using react-native-vector-icons which will do all the magic for us! The two things it needs is:

  • The name of the font
  • The Glyph Map that has been generated in the first step

Also here you need to maintain the same props as in the web implementation, with the only consideration to change the default value of size because Native applications can’t handle web units (px, em, pt). Therefore the resulting code will be something like this:

1import customFontGlyph from '../../assets/fonts/custom-font-icon.json';
2import { createIconSet } from 'react-native-vector-icons';
3
4const Icon = createIconSet(
5 customFontGlyph,
6 'custom-font-icon',
7 'custom-font-icon.ttf',
8);
9
10Icon.propTypes = {
11 name: PropTypes.string.isRequired,
12 size: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
13 color: PropTypes.string,
14};
15
16Icon.defaultProps = {
17 size: 60,
18 color: 'black',
19};
20
21export default Icon;

Rendering the icons ⚡️

As the components held the same props, the implementation for both platforms is almost identical. The differences are the values for size and the event handlers.

Web implementation

1// index.js
2import React from 'react';
3import ReactDOM from 'react-dom';
4import Grid from './components/Grid';
5import { AddCircle, Alarm, Whatshot } from './components/Icon';
6const App = () => (
7 <Grid>
8 <AddCircle color="navy" />
9 <Alarm color="orange" />
10 <Whatshot color="crimson" />
11 </Grid>
12);
13ReactDOM.render(<App />, document.getElementById('root'));
Demo React
Demo React

React Native implementation

1// index.native.js
2import React from 'react';
3import Grid from './components/Grid';
4import { AddCircle, Alarm, Whatshot } from './components/Icon';
5const App = () => (
6 <Grid>
7 <AddCircle color="navy" />
8 <Alarm color="orange" />
9 <Whatshot color="crimson" />
10 </Grid>
11);
12export default App;
Demo React Native
Demo React Native

Last words 👋

It may seem like a lot of steps, but once you have it in place, this process provides an automatic setup to easily add/remove/change icons without worrying about how developers need to use them because they will always be React Components.

I really suggest adding to .gitignore the generated files, and run this generation before every start or build process. By doing this you will ensure that all the icons placed inside the assets have the respective React and React Native component.

🚨 Get notified for my next article!

I tend to write about my challenges inside the weird, fast and hot Frontend world The challenges can be from learning a specific tool or framework to building a project from scratch.

I try to publish one article per month, but yeah sometimes life gets in the middle ... No SPAM, no hiring, no application marketing, just tech posts 👌

More articles from Ema Suriano

Make any Static Site Dynamic with Zapier

Without any doubt, there has been a huge adoption for Static Site Generators in these past 2 years, and one of the main reasons was the huge growth of Gatsby and its community.

March 19th, 2019 · 4 min read

Building a collaborative Calendar with Google and Gatsby

About one week ago a friend of mine came to me for help, he wanted to create an online calendar for cultural events around the city. The idea was to create an application with a calendar showing all the upcoming events with the possibility that any person can add or edit new events.

December 26th, 2018 · 4 min read
© 2018–2020 Ema Suriano
Link to $https://github.com/EmaSurianoLink to $https://twitter.com/EmaSurianoLink to $https://linkedin.com/EmaSurianoLink to $mailto:emanuel.suriano@gmail.comLink to $https://dev.to/emasurianoLink to $https://medium.com/@emasurianoLink to $https://www.youtube.com/c/EmaSuriano