My Experience in Building React Native Icons

6 minute read | a year ago

In this blog, I will be sharing some of the experiences I had while building React Native Icons. Here's a little background on why I created the site.

While I was doing a demo on React Native, I frequently needed icons. Each time, I had to search for icons online and convert them using the SVGR Playground. This process was tedious, involving copying or downloading the icon, uploading it to the site, and then converting it. To make this process easy for me, I thought, "Why not create a website featuring all my favorite icons?" This way, I could click on an icon, have it automatically converted to a React Native icon, and copied to my clipboard. All I would need to do is paste the icon into my project. This was the driving force behind the website.

Process of Building the Site

Before creating the site, I had two questions in mind. The first one being, how to convert an SVG to a React Native SVG? The obvious answer was to use the SVGR Playground. Luckily, the project is open source, so I decided to fork it and modify the source code to fit my needs. The other question is how to get a list of all my icons? Again the obvious answer was to use the list of icons provided by React Icons

My goal was to create a tool to automate SVG to React Native SVG conversion—a script or a CLI tool would do. Upon checking SVGR's repo, I found they already had a CLI tool.

I created a sandbox to test the CLI, and everything worked perfectly. However, I needed to save some customized metadata in the generated files (in this case .mdx files).

--- title: How does async/await work date: 10/21/23 type: Blog description: Consectetur adipisicing elit. ---

SVGR allows for custom templates, but anything within those templates must be valid JavaScript syntax, thus creating a barrier for me. So, I cloned the repo and modified the source code to be able to add metadata to the generated files:

const write = async (src, dest) => { if (!isCompilable(src)) { return { transformed: false, dest: null }; } dest = rename(dest, ext, filenameCase); const code = await convertFile(src, opts); const cwdRelative = path__namespace.relative(process.cwd(), dest); const logOutput = `${src} -> ${cwdRelative} `; if (ignoreExisting && await exists(dest)) { politeWrite(chalk.grey(logOutput), silent); return { transformed: false, dest }; } await fs.promises.mkdir(path__namespace.dirname(dest), { recursive: true }); await fs.promises.writeFile(dest, `--- name: ${dest.split("/").pop().split('.')[0]} --- \`${code}\``); politeWrite(chalk.white(logOutput), silent); return { transformed: true, dest }; };

Basically, I manipulated the data right before it was written to the file, creating a customized SVG to MDX converter.

Writing the scripts

Next, I scaffolded a Next.js project and pasted the icons I had generated. I needed to transform these .mdx files into a format digestible by Next.js. Contentlayer was perfect for converting .mdx files to .json and exporting them. However, I realized my icons were React components (string format), which aren't displayable in the browser. I wanted to use tools like Babel's JSX transformer to convert the strings to valid JSX, but I realized that it's not an optimal way of doing things. Imagine looping over hundreds of icons, transforming every single one of them, before displaying them in the browser.

At this point, I decided to go with a different approach. Instead of converting the icons to components, why not just convert them directly to strings? This approach meant I had to drop SVGR's API at this stage. I did exactly that. I built a script to convert my SVG files to MDX files, allowing me to add custom metadata to the generated content.

The script worked fine, but now I had concerns with Contentlayer. Generating lists of icons from .mdx files with Contentlayer every time I started the server seemed inefficient, especially with a large number of icons. The project has thousands of icons; every time I start the server, Contentlayer will have to generate all of those icons, which was concerning. So, I decided to drop Contentlayer as well and write my own Contentlayer-like script to generate lists of icons only if necessary.

While writing the code and doing some testing, I found issues with data duplication and order in the generated files. I spent hours trying to figure out the issue. Eventually, I realized that the problem was a mix of asynchronous and synchronous functions during the file generation process. Switching to only asynchronous functions and awaiting all of them solved the problem.

Now that both my scripts are working fine, I need to find a way to clone the icons from their repositories based on the list of icons I got from React Icons. The goal was to loop over the list of icons and clone them based on their URL. The initial code I had looked like this:

import path from "path"; import { execSync } from "node:child_process"; import { icons } from "./icons.js"; try { for (let i = 0; i < icons.length; i += 1) { const source = icons[i].source.url; const localName = icons[i].source.localName; try { execSync(`git clone ${source} ${localName}`, { stdio: [0, 1, 2], cwd: "icons", }); } catch (error) {} } } catch (error) { console.log(error); }

This script worked fine locally. However, when I deployed the code to Vercel, I got the following error message:

Error: spawnSync /bin/sh ENOENT
I tried to solve the issue with similar solutions online, but to no avail. So, I ended up using SimpleGit to handle my git commands. This package worked fine with Vercel without issues. Now the updated script looks like this:

import path from "path"; import { execSync } from "node:child_process"; import { icons } from "./icons.js"; import { convertSVGToMDX } from "./svg-to-mdx.js"; import { generateContent } from "./content-generator.js"; import simpleGit from "simple-git"; /** * * @param {string} folderName */ const handleGitPull = async (folderName) => { const git = simpleGit( path.join(process.cwd(), "icons", folderName), "master" ); console.log(`Starting pull for ${folderName}...`); if ((await git.pull())?.summary.changes) { console.log(`Changes available for ${folderName}.`); return folderName; } console.log("pull done. \n"); }; const git = simpleGit(); try { const repos = []; for (let i = 0; i < icons.length; i += 1) { const source = icons[i].source.url; const localName = icons[i].source.localName; /* First pull project, if it does not exist, then clone. */ try { const repoName = await handleGitPull(localName); if (repoName) { repos.push(repoName); } } catch (error) { if ( error.message === "Cannot use simple-git on a directory that does not exist" ) { repos.push(localName); try { console.log( "Cloning", source, "to", `${path.join(process.cwd(), "icons", localName)}` ); await git.clone( source, path.join(process.cwd(), path.join("icons", localName)) ); } catch (error) { console.log(error); } } } } console.log("Git task done... \n"); await convertSVGToMDX(repos); await generateContent(repos); } catch (error) { console.error(error.message); }

Fine-Tuning the Script

With everything working, I fine-tuned the script to also generate index.d.ts files for TypeScript. The generated types look like this.

type Icon = { name: string; folderName: string; }; export const allTypicons: Icon[];

I imported the icons, looped over them, and rendered them on the webpage. I implemented a feature to convert icons to React Native Icons and copy them to the clipboard using SVGR's API if a user clicks on an icon.

This is how I built React Native icons.

Thanks for reading! I hope you learned something new.

Happy coding...