babel-plugin-macros is a
project that I’ve followed with interest even though I’d never had a chance to
use it. Today that changed and I wanted to share my use case and my very
positive experience using it.
What is babel-plugin-macros?
The key feature of a Babel Macro is that they run at compile time. Rather than
writing JavaScript that gets bundled and executed in the browser, writing
JavaScript via babel-plugin-macros lets you run code at compile time. This means
that the code is executed on your computer when you bundle, not by your users
when they visit your website.
Most commonly these macros will either calculate some value (one that you can
and need at compilation time, not at runtime in the browser), or generate some
other code that runs in the browser.
As an example, once configured (we’ll get to that in a moment), you can use
preval.macro to easily evaluate
some code at compile time:
import preval from 'preval.macro'
const twoPlusTwo = preval`module.exports = 2 + 2`
This will be executed at compilation time, and the code that ships in your
bundle looks like this:
const twoPlusTwo = 4
But, why is this useful?
The example above is ultimately not that useful – I think we all trust browsers
to be able to add two and two at runtime. Today I came across a problem at work
that I solved with a macro which made my job much easier.
At Thread we sell clothes. Part of the site allows
users to explore our entire product listing by filtering it down to what they
are after. One of the things they can filter by is “sub category”: this is
specific types of clothes within a broader category. For example, for the
category “Shirts”, we have sub categories of “Plain shirts”, “Formal shirts”,
“Denim shirts”, and so on. The feature I’m working on adds an image to each of
these sub categories in the UI so that people who might not have heard of the
terminology can still recognise the category (before working in fashion I had no
idea what a “chambray” shirt was!).
One of the designers on the team sent me all the images, and there are a lot.
We have 50+ sub categories across all products and I had two choices for hooking
up each image to the sub category:
- Just use an image take and hard code the path:
const source = `/media/images/sub-categories/${subCategory.slug}`
- Manually create a map of
sub category slug => image URL
. This would mean
manually moving and importing 50+ images and hooking them into data from our
API. - Explore a solution that let me automatically load in the images and not have
Unsurprisingly, I picked option three, and the game was on!
Avoiding the basic solution
Just to add a bit of colour to why I avoided what on paper is the easiest
solution:
<img
src={`/media/images/sub-categories/${subCategory.slug}}`}
alt={subCategory.name}
/>
For us this approach has a major downside: we can no longer use Webpack and
ES2015 imports to manage all our assets. We have Webpack configured to take our
images and move them into the right place, and I didn’t want to have to special
case one folder of images just to make using them a little bit easier.
Setting up babel-plugin-macros
You might think that the macros need some complex setup but nope, it’s as easy
as:
yarn add babel-plugin-macros
- Add
'macros'
to your plugins list in your babel config.
And that’s it 👌.
Sub category slugs
Each sub category is an object with a few keys:
{
name: 'Denim shirts',
slug: 'denim-shirts',
id: 'abc123',
}
Thankfully I’d already discussed with our designer that we’d name the images
based on the slugs, so I knew that I had all the images mapped and ready. This
helped a lot and it’s something I’d recommend when working with a designer who
is creating a bunch of assets: chat ahead of time to figure out the best format
and naming scheme for sharing the results.
import-all.macro
The final piece of the puzzle is the
import-all.macro package. This
lets me generate a list of imports from a folder at compile time. For example:
import importAll from 'import-all.macro'
const a = importAll.sync('./files/*.js')
Gets turned into something like this at compile time:
import * as _filesAJs from './files/a.js'
import * as _filesBJs from './files/b.js'
const a = {
'./files/a.js': _filesAJs,
'./files/b.js': _filesBJs,
}
This is exactly what we want! We can use importAll
to create an object of all
the file paths and the image URLs – We have Webpack set up so that when we
import an image we get back the full path of where that image will be put during
build:
import image from './image.jpg'
// image => /media/images/image.jpg
Once I’d figured this out, I was ready to write some code 🎉.
Dealing with nested folders
To make the folder of images easier to work with we’d agreed to nest sub
categories under a folder of that category. This meant that I needed to do a bit
of data manipulation to get exactly what I wanted, because the file name
returned from import-all.macro
would have that extra folder in:
const images = importAll.sync('./category_images/**/*.png')
// images looks like:
{
'./category_images/shirts/denim-shirt.png': '/media/images/category_images/shirts/denim-shirt.png',
...
}
And what I wanted to end up with was a map where the key is purely the slug:
// this is what we want
{
'denim-shirt': '/media/images/category_images/shirts/denim-shirt.png',
...
}
This was a case of doing a bit of work on the object that import-all.macro
generates for us:
import importAll from 'import-all.macro'
const allCategoryImages = importAll.sync('./category_images/**/*.png')
const imagesMap = new Map(
Object.entries(allCategoryImages).map(([fileName, imageUrl]) => {
// image = "./category_images/accessories/bags.png"
// so split and pick out just the "bags.png" bit
const subCategory = fileName.split('/')[3]
// remove the extension and return [key, value] pair of [slug, imageURL]
return [subCategory.replace(/.png/, ''), imageUrl]
})
)
export default imagesMap
And with that, we’re done! Now in our React component we can fetch the image
from our Map:
const imageUrl = imagesMap.get(subCategory.slug)
As a bonus, we can also easily add some logging to alert us to if a sub category
is missing an image:
if (imageUrl.has(subCategory.slug) === false) {
logError('...')
}
Conclusion
The solution that babel-plugin-macros lets us create is elegant and easy to work
with. It will also automatically deal with new images and new sub categories and
it’s easy for non-engineers to update a sub category image without needing any
help from us – they can just dump the new image in the right place and
everything will update. For tasks like this in the future we will definitely be
reaching for it again and I recommend giving it a go next time you’re faced with
a much of manual lifting that feels very much like it could be automated!
Subscribe to MarketingSolution.
Receive web development discounts & web design tutorials.
Now! Lets GROW Together!