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

Ubuntu 20.04 on a 2015 15" MacBook Pro

I decided to give Ubuntu 20.04 a try on my 2015 15" MacBook Pro. I didn't actually install it; I just live booted from a USB thumb drive which was enough to try out everything I wanted. In summary, it's not perfect, and issues with my camera would prevent me from switching, but given the right hardware, I think it's a really viable option. The first thing I wanted to try was what would happen if I plugged in a non-HiDPI screen given that my laptop has a HiDPI screen. Without sub-pixel scaling, whatever scale rate I picked for one screen would apply to the other. However, once I turned on sub-pixel scaling, I was able to pick different scale rates for the internal and external displays. That looked ok. I tried plugging in and unplugging multiple times, and it didn't crash. I doubt it'd work with my Thunderbolt display at work, but it worked fine for my HDMI displays at home. I even plugged it into my TV, and it stuck to the 100% scaling I picked for the othe

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.&quo

Haskell or Erlang?

I've coded in both Erlang and Haskell. Erlang is practical, efficient, and useful. It's got a wonderful niche in the distributed world, and it has some real success stories such as CouchDB and jabber.org. Haskell is elegant and beautiful. It's been successful in various programming language competitions. I have some experience in both, but I'm thinking it's time to really commit to learning one of them on a professional level. They both have good books out now, and it's probably time I read one of those books cover to cover. My question is which? Back in 2000, Perl had established a real niche for systems administration, CGI, and text processing. The syntax wasn't exactly beautiful (unless you're into that sort of thing), but it was popular and mature. Python hadn't really become popular, nor did it really have a strong niche (at least as far as I could see). I went with Python because of its elegance, but since then, I've coded both p