Refactor like no one is watching

Apr 19, 2020 • 6 min read

It is hard to underestimate an importance of AST transformation tools in the frontend now:

  • we use babel

  • we use eslint to enforce best practices or avoid mistakes/bugs

  • we use prettier to format the code

But today I wanna tell a short story about using AST transformation to refactor the code. Like some codemods you might used.

Backstory

At tourlane we use styled-components (probably, the most popular CSS-IN-JS solution in react) along with styled-system (awesome utility belt for writing responsive styles based on scales from global theme), so we can build components like this:

let User = ({ avatar, name }) => (
  <Flex flexDirection={['column', 'row']}>
    <Box as="img" mb={[1, 0]} mr={[0, 1]} src={avatar} />
    <Box p={1}>{name}</Box>
  </Flex>
)

While some of you might find this style of writing components pretty controversial, this is not the point of this article. But I still can recommend a couple articles to get more reasoning behind it:

The point of the code block from above is the usage of responsive CSS values: jsx±flexDirection={['column', 'row']}(pretty handy isn't it?). Under the hood it will use media breakpoints provided in the theme to compile the responsive styles like:

.some-generated-class {
  flex-direction: column;
}

@media screen and (min-width: 64em) {
  .some-generated-class {
    flex-direction: row;
  }
}

The problem

When we started one of our projects we didn't have any styles specific to tablets, let's say we had something similar to this:

let breakpoints = [
  // no need to define lower breakpoint
  // since we use mobile-first
  // (styles applied without media are considered to be mobile)
  
  '64em', // desktop
  '80em', // wide
]

let Layout = ({ children }) => (
  <ThemeProvider theme={{ breakpoints }}>
    <Header />
    {children}
    <Footer />
  </ThemeProvider>
)

But lately we got a new page where the mobile and table designs are quite different. But our scale system doesn't support it 😧: we can not use responsive attributes that we are used to 😨. Adding one more breakpoint will break all existing components 😰.

I bet you've been in a situation like this. E.g. you might used some library api which was deprecated. Luckily in cases like this, library authors usually prepare codemods which will update your codebase for you. So you don't even have to understand what it does under the hood.

But in our case there was no one to rely on, nobody to blame... but... me? 🥺

jscodeshift to the rescue

Even though we could write tablet specific styles "by hand" - using normal styled-components syntax. It wasn't the way we wanted to proceed - we really like the api provided by styled-system, moreover we didn't want to introduce ambiguity in writing responsive styles. And obviously we weren’t too keen to go through the whole codebase and change all usages of responsive props.

Sounds like a perfect task for codemode!

I have never worked with them, but I've heard that jscodeshift is a cool tool to build them, e.g. react team uses it. All you have to do is create js/ts file and export the transform function which will take care of required AST transformations.

Another important part is, of course, a general understanding of AST: what it is, what it consists of and how we can alter it. I found this babel handbook to be super useful intro. Another must-have tool is AST explorer. With help of this tool you can write some code to see its AST representation and write transform functions with immediate results!

Also I can hardly imagine writhing AST transformation without typescript (you can get info about nodes and their properties, right during typing intead of switching between editor and bable docs all the time), so I installed its typings (@types/jscodeshift) along with a library itself.

And after about an hour of playing with it I built this:

import { Transform } from 'jscodeshift';

let directions = ['', 't', 'r', 'b', 'l', 'x', 'y'];
// these are responsive attrs provided by styled-system
let spaceAttributes = [
  ...directions.map(d => `m${d}`),
  ...directions.map(d => `p${d}`),
  'flexDirection', 'justifyContent', // ...
];

let transform: Transform = (fileInfo, { j }) =>
  j(fileInfo.source)
    .find(j.JSXAttribute)
    .forEach(({ node }) => {
      let attributeName =
        typeof node.name.name === 'string' ? node.name.name : `¯\\_(ツ)_/¯`;
      let { value } = node;

      if (
        spaceAttributes.includes(attributeName) &&
        // we are interested only array expressions which has more then 1 value
        // like <Box m={[8, 16, 24]}>
        value?.type === 'JSXExpressionContainer' &&
        value.expression.type === 'ArrayExpression' &&
        value.expression.elements.length > 1
      ) {
        let [xs, ...otherMedias] = value.expression.elements;

        // null in styled-system means - do not introduce new media query,
        // so thanks to mobile first approach we'll have values defined in xs.
        // I could also write [xs, xs, ...otherMedias],
        // it will just result in bigger css output
        value.expression.elements = [xs, j.identifier('null'), ...otherMedias];
      }
    })
    .toSource();

export default transform;

You can also play with live example in AST explorer.

As you can see the transform is not that big and yet very descriptive. Obviously it doesn't cover all possible cases, e.g. we could use ternary expressions in jsx attributes or use variables referring to arrays and etc. But it does explain the idea, at least I hope so.

The cool thing is you can run your codemode ➡️rollback using git ➡️improve 🔁1000 times, until you are happy with the result.

Make codemods part of your toolbelt

That is all I wanted to share today. I hope after reading this article you will consider codemods to be not just powerful tool, but also a thing that is easy to learn, as I did. If you have any questions or suggestion feel free to use comments section or rich me directly in twitter. Thanks!

Next reads

Enjoy the little things

Mar 22, 2020 • 6 min read

Well, looks like now, with all this self isolation caused by COVID-19, I'll have some time to code and maybe even to write a couple of articles about it. This one will be the first one. In it I'm gonna play with react-spring to create simple digital clock animation.

React SSR pitfalls in building adaptive layouts

Oct 6, 2019 • 4 min read

Short story about issues we faced while using adaptive design in statically generated app. And about approaches to avoid them.

The simplest example of scheduling on the main thread

Feb 19, 2019 • 4 min read

I have recently read several articles related to relatively new react reconciliation algorithm (aka react fiber). And I wondered how to implement something really simple to explain idea of scheduling and splitting work into chunks. So here is my thoughts.