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 can a hard task, specially when it comes to have the same workflow to add/remove/use an icon in web and native applications (Android/iOS). In this post, I’m going to share my experience of implementing a consisting icon system inside the component libraries of my company.

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 workaround the community has created 3rd party libraries outside from 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 render in both platform (web and mobile), and also depending on the RN 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: Autogenerated Icons 👍

Let’s set the example that you have all your in SVG format and inside an assets folder inside your repository. 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 automatize this process, so the only thing you need to do is adding/removing Icons from this assets folder. This is when generating Icons are quite helpful!

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 of it will be a file which exports all the icons as React Components, each of them will call the proper implementation in the respective platform.

At the same, both implementation will implement the same API (propTypes) making the icon system consistent between platforms and proving an excellent development experience.

Demo time

I created a repository from which I extracted the snippets that are going to follow, so in case you want to skip the explanation and jump into the code I leave you the link.

Final Demo

Repo link Live web demo

Time to code

Time to start

So let’s start a clean project using create-react-app and create-react-native-app. After playing around a little 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.

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

Then inside the folder you need to add the /public folder with the delete-me-later project. Also, create the assets folder. For this demo I decided to use the collection of Icon called Landmarks and Monuments from FlatIcon. Download the package and extract the icons inside the assets folder.

Generating Icon Components

This will be the entry point for both platform, each svg icon inside the assets folder will be transform into a React component which 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 then. Otherwise when you try to run your project it will throw an exception because it doesn’t know how to handle this type of files.

As I decided to use as a base project create-react-app it has already integrated an svg loader inside its hidden webpack configuration. The way this 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. In order do you can use the following 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 Louvre } from './001-louvre.svg';
2import { ReactComponent as LeaningTowerOfPisa } from './002-leaning tower of pisa.svg';
3import { ReactComponent as Coliseum } from './003-coliseum.svg';
4
5// and the list continues ...
6
7export default {
8 '001-louvre': Louvre,
9 '002-leaning tower of pisa': LeaningTowerOfPisa,
10 '003-coliseum': Coliseum,
11};

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

For this example the shared props between platform are:

  • name: The name of 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, also here you can set the default size of it.
  • color: The general color of it, 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 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 the 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 is a map that has keys as the name of the icon and values equals to the character/position of it inside the font.

There is a little hack you might to do the generated Glyph map because the values of each icon is expresed in Hexa and React Native has problem to read it. The simple solution is to parse the 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`,
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, first add the following property to your package.json:

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

And then execute the command of 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 defualt value of size because Native applicaitons 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 implemntation for both platform is almost identical, the differences are the values for size and the event handlers.

Web implemntation

1import ReactDOM from 'react-dom';
2import React from 'react';
3import { Coliseum, BigBen, Gherkin } from './components/Icon';
4
5const App = () => (
6 <div>
7 <Coliseum />
8 <BigBen
9 color="lightblue"
10 size="8em"
11 onClick={() => alert('This is the event handler')}
12 />
13 <Gherkin color="pink" />
14 </div>
15);
16
17ReactDOM.render(<App />, document.getElementById('root'));

Demo React

React Native implemntation

1import React from 'react';
2import { SafeAreaView } from 'react-native';
3import { Coliseum, BigBen, Gherkin } from './components/Icon';
4
5const App = () => (
6 <SafeAreaView>
7 <Coliseum />
8 <BigBen
9 color="lightblue"
10 size={100}
11 onPress={() => alert('This is the event handler')}
12 />
13 <Gherkin color="pink" />
14 </SafeAreaView>
15);
16
17export default App;

Demo React Native

Last words 👋

It may seems like a lot of steps but once you have it in place this process will allow to easily add/remove/change icons without worrying about how they are going to be manipualate because they are going to be always React Components.

One last tip, I really suggest to 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

Building a maintainable Icon System for React and React Native

Maintaining Icons in React and React Native can be a hard task, in this post I share the experience of implementing a consisting icon system inside the component libraries of my company.

October 1st, 2019 · 5 min read

Using Storybook as a powerful Visual Testing Platform

My experience working with this strategy of testing (which does not replace the others) and the integration with my current development tool.

October 1st, 2019 · 6 min read
© 2019 Ema Suriano
Link to $https://github.com/EmaSurianoLink to $https://twitter.com/EmaSurianoLink to $https://linkedin.com/EmaSuriano