Automating SVG to JSX conversion with svgr
Transforming SVG files into JSX is boring and prone to error. We can handle it better with svgr. Its defaults are good enough, but you’ll likely need to customize it for your needs, which is made possible by writing a template.
These templates are babel plugins. Learning how to build one can be intimidating since it’s a huge topic, but it’s worth it.
In this blog post I want to share a template that converts this SVG file:
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="" /></svg>
…into this React component:
import React from "react";import { SvgIcon, SvgIconProps } from "@material-ui/core"; export const SvgComponent: React.FC<SvgIconProps> = (props) => { return ( <SvgIcon viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {...props}> <path d="" /> </SvgIcon> );};
So we need to wrap the SVG around the SvgIcon component from Material-UI library, with type annotations and we need to override props using the spread syntax.
Template
Here’s the template that worked for me, followed by the explanation.
const { identifier, tsTypeAnnotation, tsTypeReference, tsTypeParameterInstantiation, jsxClosingElement, jsxElement, jsxIdentifier, jsxOpeningElement, jsxSpreadAttribute,} = require('@babel/types') const template = ( { template }, opts, { imports, componentName, props, jsx, exports },) => { const plugins = ['jsx', 'typescript'] const typescriptTemplate = template.smart({ plugins }) const wrappedJsx = jsxElement( jsxOpeningElement(jsxIdentifier('SvgIcon'), [ ...jsx.openingElement.attributes, jsxSpreadAttribute(identifier('props')), ]), jsxClosingElement(jsxIdentifier('SvgIcon')), jsx.children, false ) componentName.typeAnnotation = tsTypeAnnotation( tsTypeReference( identifier('React.FC'), tsTypeParameterInstantiation([tsTypeReference(identifier('SvgIconProps'))]) ) ) return typescriptTemplate.ast` import React from 'react' import { SvgIcon, SvgIconProps } from '@material-ui/core' export const ${componentName} = (props) => { return ( ${wrappedJsx} ) } `} module.exports = template
Setting up plugins
We need to use the TypeScript plugin:
const plugins = ['jsx', 'typescript'] const typescriptTemplate = template.smart({ plugins })
Building the JSX
Now, the jsx part. Our goal is to replace the built-in svg
element with the
SvgIcon
component. We can do this by creating a new
jsxElement
, change its
opening and
closing elements to
be SvgIcon
and reuse the child elements (don’t mind doing this recursively,
but we could I guess).
const wrappedJsx = jsxElement( jsxOpeningElement(jsxIdentifier('SvgIcon'), [ ...jsx.openingElement.attributes, jsxSpreadAttribute(identifier('props')), ]), jsxClosingElement(jsxIdentifier('SvgIcon')), jsx.children, false)
You’ll notice how we reuse the same attributes from the original jsx opening
element and also spread props
into them using
jsxSpreadAttribute.
Writing type annotations
I thought that this would work:
// ... return typescriptTemplate.ast` import React from 'react' import { SvgIcon, SvgIconProps } from '@material-ui/core' export const ${componentName}: React.FC<SvgIconProps> = (props) => { return ( ${wrappedJsx} ) }`
But the type annotation, : React.FC<SvgIconProps>
is stripped out from the
final output file.
Then I went with this hack:
componentName.name = 'SvgComponent: React.FC<SvgIconProps>'
This works but it doesn’t feel right… This seems to be the proper, although more verbose, way:
componentName.typeAnnotation = tsTypeAnnotation( tsTypeReference( identifier('React.FC'), tsTypeParameterInstantiation([tsTypeReference(identifier('SvgIconProps'))]) ))
To come up with this, I needed to learn how to properly build an AST with Babel.
Usage in Vim
If you use Vim, you can convert a file using %!npx @svgr/cli --template path/to/template.js
. In case you didn’t know, this is a built-in feature
called filter.
You could also configure your project to use a template by default with a
.svgrrc
file at the
project’s root folder:
// .svgrrc.jsmodule.exports = { template: require('./path/to/template.js')}
Usage in VS Code
svgr
also has a VS Code
extension.
But, if you prefer, you could use the Edit With Shell
Command
extension, which is similar to Vim’s filter feature.
Pending improvements
Here are some improvements I couldn’t figure out how to do/don’t care so much, but it would be nice to have:
- Use the file name as component name.
- Retain empty lines in the final output.
- Remove semicolons.
Right now this still requires some manual labor to get 100% right, but it’s ok.