Skip to main content

JavaScript: Porting from react-css-modules to babel-plugin-react-css-modules (with Less)

I recently found a bug in react-css-modules that prevented me from upgrading react-mobx which prevented us from upgrading to React 16. Then, I found out that react-css-modules is "no longer actively maintained". Hence, whether I wanted to or not, I was kind of forced into moving from react-css-modules to babel-plugin-react-css-modules. Doing the port is mostly straightforward. Once I switched libraries, the rest of the port was basically:
  • Get ESLint to pass now that react-css-modules is no longer available.
  • Get babel-plugin-react-css-modules working with Less.
  • Get my Karma tests to at least build.
  • Get the Karma tests to pass.
  • Test things thoroughly.
  • Fight off merge conflicts from the rest of engineering every 10 minutes ;)
There were a few things that resulted in difficult code changes. That's what the rest of this blog post is about. I don't think you can fix all of these things ahead of time. Just read through them and keep them in mind as you follow the approach above.

.babelrc configuration

My .babelrc configuration looks like this:
{
    "presets": [
        "..."
    ],
    "plugins": [
        [
            "react-css-modules",
            {
                "filetypes": {
                    ".less": {
                        "syntax": "postcss-less"
                    }
                },
                "generateScopedName": "[name]--[local]--[hash:base64:5]"
            }
        ]
    ]
}
Note that Babel uses postcss-less at build time, but that's separate of the code that compiles the Less for things like extract-text-webpack-plugin.

The generateScopedName was an important bit of configuration in order to get the CSS class names to be consistent between build time and extract-text-webpack-plugin. This matches localIdentName: '[name]--[local]--[hash:base64:5]' in our CSS loader options.

Simple changes to your React components

Don't import 'react-css-modules'.

Don't use the cssModules decorator or function.

Generally, if you have an import line like import styles from './hello-colorful.less';, change it to import './hello-colorful.less';. You only need to use names if you import more than one .less file.

Resolving .less files.

babel-plugin-react-css-modules isn't very good at respecting your Webpack resolver roots. Hence, it's best to use relative paths when referring to .less files.

Removing the @cssModules decorator causes subtle API changes

It used to be with @cssModules, you could just return undefined in your render method (or fall through without a return statement). Now you have to explicitly return null. Also, with the @cssModules decorator, if your component returned null, it'd convert that to <noscript></noscript>. This results in changes to your tests:
expect(wrapper.text()).toBe('');  // Or
expect(wrapper.type()).toBe('noscript');
Becomes:
expect(wrapper.html()).toBe(null);
Similarly, you can no longer pass undefined to styleName. Hence, if value might be null, then:
<div styleName={ value }>
Becomes:
<div styleName={ value || '' }>
If you see an error message such as "Cannot read property 'split' of undefined", it probably means you're passing undefined to styleName instead of ''. And in your tests, if you previously were expecting className to be undefined, it'll now be ''.

Don't get too tricky when setting styleName

For instance:
buttonProps.styleName = ...
...
<Button { ...buttonProps }>
Becomes:
<Button { ...buttonProps } styleName=...>
Similarly, you should unpack className as well:
<Popover styleName="box" { ...props }>
Becomes:
// className has to be its own thing when used with styleName and
// ...props. Otherwise babel-plugin-react-css-modules gets confused.
<Popover { ...props }
    styleName="box" className={ props.className }>

Class names are a little different

Testing for class names in your tests is a little different. Hence:
expect(findElementByDataPurposeAttribute('header-bottom'))
    .toHaveClassName(styles['header-bottom--progress']);
Becomes:
expect(findElementByDataPurposeAttribute('header-bottom').node.className)
    .toContain('header-bottom--progress');
For normal HTML elements, it's a little different:
expect(findElementByDataPurposeAttribute('helpfulness-thanks'))
    .toHaveClassName(styles['thanks--hidden']);
Becomes:
expect(findElementByDataPurposeAttribute('helpfulness-thanks').node.props.className)
    .toContain('thanks--hidden');

Fancy Webpack imports for .less files no longer work

If you had weird imports such as:
import icomoonCss from '!!raw!./icomoon.global.less';  // Or
import themeVariables from '!less-vars!./variables.global.less';
That'll no longer work because babel-plugin-react-css-modules will try to actually resolve those imports as normal files.

To work around this problem, you can create a new file, icomoon-global-less-exporter.js, that does an immediate export such as:
export { default } from '!!raw!./icomoon.global.less';
Now, import icomoon-global-less-exporter.js instead:
import icomoonCss from './icomoon-global-less-exporter';
The level of indirection is enough to work around the problem.

See this bug for more details.

Debugging invalid CSS class names

babel-plugin-react-css-modules doesn't output enough information when you use invalid CSS class names. I found it helpful to just hack the source code in node_modules to output a little bit more information when I got stuck. I hacked the source code in either node_modules/babel-plugin-react-css-modules/dist/getClassName.js or node_modules/babel-plugin-react-css-modules/dist/browser/getClassName.js. In getClassNameForNamespacedStyleName, I added either a console.log statement or included more information in the exceptions.

Update: They accepted my patch to improve the error messages.

Similarly, MobX can be a little unhelpful when render raises an exception. I tweaked node_modules/mobx/lib/mobx.js so that anytime I saw finally, I added:
catch (e) { debugger; }
Those changes were enough to debug all of my errors.

Using & in .less files

Our .less code confused postcss-less a bit. Hence:
.strength-box { ... &--ok { } }
Becomes:
.strength-box--ok { }
We had actually deprecated the previous pattern anyway since it makes the code harder to grep.

Mutating props

For whatever reason, you can no longer mutate props in a test and then call componentWillReceiveProps:
wrapper.node.props.value = 'zwek';
wrapper.node.componentWillReceiveProps(wrapper.node.props);
Becomes:
const newProps = {
    ...wrapper.node.props,
    value: 'zwek',
};
wrapper.node.componentWillReceiveProps(newProps);

Funky psuedo inheritance in .less

I saw a bunch of cases where a React component would import a .less file. Then, that .less file would import another .less file with real CSS in it. In effect, this provided a sort of inheritance. However, it also meant that the same base CSS was getting output in multiple places. Furthermore, it confused postcss-less.

Hence, I broke apart this inheritance and just made use of each .less file separately in the React component:
import baseStyles from './base.less';
import styles from './my-component.less';
...
<div styleName="baseStyles.extended-css-class styles.extended-css-class
    styles.some-other-css-class">
In my-component.less, you can still use (reference) in order to import variables and mixins. You just can't import actual CSS code.
@import (reference) './base.less';
It kind of sucks to lose this implicit inheritance. However, fixing the underlying performance gotcha and being a little more explicit is a win.

Comments

Popular posts from this blog

Drawing Sierpinski's Triangle in Minecraft Using Python

In his keynote at PyCon, Eben Upton, the Executive Director of the Rasberry Pi Foundation, mentioned that not only has Minecraft been ported to the Rasberry Pi, but you can even control it with Python. Since four of my kids are avid Minecraft fans, I figured this might be a good time to teach them to program using Python. So I started yesterday with the goal of programming something cool for Minecraft and then showing it off at the San Francisco Python Meetup in the evening.

The first problem that I faced was that I didn't have a Rasberry Pi. You can't hack Minecraft by just installing the Minecraft client. Speaking of which, I didn't have the Minecraft client installed either ;) My kids always play it on their Nexus 7s. I found an open source Minecraft server called Bukkit that "provides the means to extend the popular Minecraft multiplayer server." Then I found a plugin called RaspberryJuice that implements a subset of the Minecraft Pi modding API for Bukkit s…

Apple: iPad and Emacs

Someone asked my boss's buddy Art Medlar if he was going to buy an iPad. He said, "I figure as soon as it runs Emacs, that will be the sign to buy." I think he was just trying to be funny, but his statement is actually fairly profound.

It's well known that submitting iPhone and iPad applications for sale on Apple's store is a huge pain--even if they're free and open source. Apple is acting as a gatekeeper for what is and isn't allowed on your device. I heard that Apple would never allow a scripting language to be installed on your iPad because it would allow end users to run code that they hadn't verified. (I don't have a reference for this, but if you do, please post it below.) Emacs is mostly written in Emacs Lisp. Per Apple's policy, I don't think it'll ever be possible to run Emacs on the iPad.

Emacs was written by Richard Stallman, and it practically defines the Free Software movement (in a manner of speaking at least). Stal…

ERNOS: Erlang Networked Operating System

I've been reading Dreaming in Code lately, and I really like it. If you're not a dreamer, you may safely skip the rest of this post ;)

In Chapter 10, "Engineers and Artists", Alan Kay, John Backus, and Jaron Lanier really got me thinking. I've also been thinking a lot about Minix 3, Erlang, and the original Lisp machine. The ideas are beginning to synthesize into something cohesive--more than just the sum of their parts.

Now, I'm sure that many of these ideas have already been envisioned within Tunes.org, LLVM, Microsoft's Singularity project, or in some other place that I haven't managed to discover or fully read, but I'm going to blog them anyway.

Rather than wax philosophical, let me just dump out some ideas:Start with Minix 3. It's a new microkernel, and it's meant for real use, unlike the original Minix. "This new OS is extremely small, with the part that runs in kernel mode under 4000 lines of executable code." I bet it&…