clock menu more-arrow no yes mobile
React background

Filed under:

How We Rewrote a Vue App with React and Tailwind in 21 Days

Why we did it, how we did it, and what we learned.

In March 2019, the Concert Platforms team started building a new, exciting web application in Vue. After a year of internal testing and iteration, we started to overhaul the app with an expanded feature set and a fresh design.

In January 2020, we started to implement the changes.

Then, we rewrote the entire app from the ground up in React and Tailwind.

This is our story.

First of All: What Were We Thinking?

Since the dawn of time, developers have been tricked into choosing the newest, shiniest technology. As the codebases we work on start to feel complex and wrought with legacy requirements, a virtual expiration sticker appears — and we find ourselves ripping everything out and starting over.

After months and months of work, the shiny new rewrite prevails. Hooray! The celebration lasts about a week, and then we realize we’re left the same problems that existed when we started.


In general, rewriting your application from scratch is a really, really bad idea. But we did it anyway.

One of the factors driving this decision was staffing requirements. Ramping up to a product launch later in 2020, our team needed to add both full-time and contract engineers to support our work.

When we interviewed full-time engineers, we found that 90% of them knew React or preferred React over Vue. On top of that, 100% of the engineers who made it to the technical portion of our interview chose the React version of our client-side hiring exercise over the Vue version.

And the contract engineers we interviewed knew React instead of Vue. While we invest in our employees to give them training they need, we didn’t want to spend our contracting budget having contractors learn Vue from scratch.

At Vox Media, we try to hire quality, well-rounded engineers rather than seek out experts in a certain technology. We train new full-time engineers to use our evolving tech stack and support their professional development to grow in those areas.

But our immediate staffing requirements and the trends we saw in the hiring market certainly made an impact on our decision.

Goodbye Vue; Hello React

Image with text: React: A JavaScript library for building user interfaces

Two short years ago, I sat in front of this very keyboard and described to you, dear blog reader, why I thought Vue was better than React.

Go ahead, check it out. It’s at the very beginning of the post.

One of my main reservations was the awkwardness of the JSX syntax. Today, I’ll confirm that writing XML-like tags instead JavaScript files was weird — at first. But then I got used to it, and fell in love with it. After all, JSX is just JavaScript with some shortcuts sprinkled in.

At the same time, I touted Vue’s single-file components and template syntax as having a shallower learning curve. While I still think Vue’s template syntax is approachable, I’m no longer convinced that the syntax is any easier to learn than JSX.

In fact, Vue templates require you to learn a new hybrid syntax for property binding and event listening, along with more cognitive overhead to connect props, computed props, data, watchers, and methods in the template tag with Vue’s proprietary object syntax in the script tag.

Templating concerns aside, a bigger reason for switching to React was a fundamental paradigm shift in its component architecture: React Hooks.

React Hooks Are a Game-Changer

In 2018, Hooks were announced as part of React 16.8 as a way to manage state inside functional components without requiring the use of an ES6 JavaScript class.

Hooks mark a major departure from traditional class-based React components.

Functional components are smaller in lines of code and quicker to compose. They are also more approachable, because they don’t require you to understand the nuances of ES6 classes or memorize lifecycle method names.

Suddenly, instead of bemoaning the complexity of React’s class-based components and preferring the “simpler” Vue template syntax, I found myself falling in love with writing small, functional components in React.

With Hooks at our disposal, the cost of creating new components is lowered significantly. We can start writing a component, pull in other components on a whim (powered by smart IDE auto-imports), and refactor a single component into multiple components in the same file.

One issue we discovered with Vue was that we incurred a tax when we created new components and wanted to consume them in other places. This is because Vue does not allow you to create multiple components in the same single-file component.

To demonstrate, let’s take this simple Vue form component with a couple input fields:

Let’s say we wanted to split out the input fields into a subcomponent for less boilerplate code.

In Vue, since we can’t easily define multiple components in a single file, we have to create a new file entirely:

Then we have to import it into our original component. Vue adds another requirement: we have to register the component in addition to importing it and using it in the template.

This certainly isn’t the end of the world!

But it’s little things like the above example which add development time — in addition to creating more files in a project. Plus, we lose IDE support for typing a component name and expecting it to import automatically.

Here’s the same example, written in React:

And here’s an example of VS Code’s auto-import feature helping us author React components:

In addition to small, composable components, React has shined in its use of custom Hooks and React Context.

Our application contains a complex set of field inputs driven by a remote JSON schema whose values are stored in a denormalized JSON payload.

With this complex schema, we found it all-too-easy to make each individual component, like InputField, bear the brunt of the state management and data persistence responsibility.

Instead of falling into that trap with React, we decided to leverage a custom hook we built called useAssets which allows the consumer component to pass its entire field schema object to the hook and get a useful payload in return: value, the current value of the field from the asset payload; and setValue, the setter method for new values.

This allows us to write short, concise components, and offload to our custom hook the complexity of parsing the field schema, updating input values, and persisting those values to the server.

The useAssets hook is powered by React Context, which is React’s recommended way of handling inter-component communication between many layers of components. It’s a great alternative to prop drilling.

This is much different than our approach in the Vue app, where we supported this complex schema with Vuex, mixins and inheritance, and lots of computed properties and watchers.

This quickly became unwieldy:

  • Vue has a similar concept of component composition with provide and inject, but it doesn’t recommend it be used for common applications — and it doesn’t support sending data back up the component tree.
  • Instead, Vuex is a recommended solution for managing state in large, complex component trees. While this technically worked, it was also more overhead managing the state in different getters and mutations. We also found it tedious to constantly be referencing this.$store, or importing and merging mapGetters in individual components.
  • Additionally, while Vue mixins and inheritance allowed us to abstract away a lot of the functionality, it made it confusing to know where a value in the template was being set or computed.
  • Vue promotes the concept of watchers, which are a great way to cause side effects when an object value changes. While great in theory, our watchers — combined with two-way data binding and lots of computed properties — ended up causing more problems (read: infinite loops) than we anticipated. By using a more explicit one-way data binding mechanism promoted in React, we felt more comfortable making changes and building our complex component tree.

Goodbye Bespoke CSS; Hello Tailwind

A screenshot of the Tailwind CSS homepage

In a decade of web development, I have never seen such a polarized reaction to a new concept than I have with utility-based CSS frameworks like Tailwind:

When reading the code snippet above, did you have any of the following reactions?

  • “What the heck is GOING ON?”
  • “Why would anyone do this? Isn’t this just like writing inline styles?”
  • “Won’t this mean a TON of repeated code everywhere?”
  • “What happens if you need to change the color of that text everywhere?”
  • “This is not semantic HTML and CSS!”
  • “Why do you hate the cascade?”
  • “This smells like something back-end developers use just so they can avoid learning CSS.”
  • “This is only for prototyping small projects, right? Surely you’re going to rewrite this with real CSS?”

These are all totally normal reactions.

I felt this way, too.

Then something changed. Once I started learning the Tailwind utility conventions, I felt much more powerful when composing components. I was able to focus entirely on the component authoring process because everything — JavaScript logic, templating, and styles — happened in a single place.

Suddenly, this front-end engineer before you today — who spent a decade being proud of his hand-crafted, artisanal BEM-compliant CSS — who loved using Sass with lots of spacing variables, typography mixins, and partials — who loved promoting semantic class names with modifiers and having complete control over the way his CSS classes were composed — would happily never write a line of custom CSS again thanks to Tailwind CSS.

Tailwind CSS complements React really well. Unlike Vue, which has a wonderful, opinionated method for composing styles in a standard style tag, React has nothing. Zilch. You’re on your own, buddy.

There are tons of CSS-in-JS solutions meant to pair with React, but Tailwind is great because you don’t have to change anything about your build process or component creation — you just add class names.

Above all else, I think the greatest benefit Tailwind offers our engineering team is a strong set of guardrails. This comes in the form of a limited set of spacing, color and responsive layout utilities.

Sure — you can accomplish the same thing by using Sass variables in a standard CSS project. But even that requires you to become familiar with the variable naming convention in your particular project, and it’s time-consuming to set up a complete index of color shades, typography sizes, and spacing utilities in Sass.

When using Tailwind with VS Code, a popular extension allows you to autocomplete class names — and even see a preview of the color palette and what CSS properties the classname will apply!

Finally, I would be remiss if I didn’t revisit some of the initial reactions I had to using Tailwind for the first time and address how I feel about them now:

“What the heck is GOING ON?”

Here’s what’s going on:

The Tailwind utility classes tell a story about how the component is meant to be displayed. Rather than opening up my stylesheets folder, digging through the subfolders until I find the right partial, and cross-referencing a classname like .author-bio, Tailwind tells me immediately:

  • The text is centered
  • The heading is larger than normal text
  • The heading has a smidgen of margin below it
  • The heading is bold
  • The paragraph text is gray. Specifically, it’s the 600 shade of gray (where 100 is lightest, and 900 is darkest). My experience also tells me that the 600 shade of any color is dark enough on a white background to meet accessibility contrast guidelines.

“Why would anyone do this? Isn’t this just like writing inline styles?”

Right?! It seems silly. But here’s what I’ve found:

Tailwind’s utility classes are concise, yet human-readable.

For example, “block” equates to “display: block” in CSS. By removing a colon and making some properties a single CSS word, styling components with Tailwind is way easier than writing inline styles — especially if you’re using React, where writing inline style objects is not fun.

To top it off, Tailwind’s classes can be customized to meet your needs. For example, we can define different shades of teal in a config file so that when Tailwind classes are generated, our custom “Concert” shade of teal is used.

This allows us to keep Tailwind’s pre-defined utility classes without requiring engineers to learn all of our color variables.

Tailwind’s creators have a much-better explanation of the utility-first approach in their documentation.

“Won’t this mean a TON of repeated code everywhere?”

Yeah, totally — if you’re building an application that doesn’t leverage any sort of component system.

But who is building these mythical, component-less applications? At Vox Media, even our decade-old Chorus code leverages server-side Rails components to be able to scale our design systems across all of our sites.

When using a component framework like React or Vue, it’s trivial to spin up a new component when you realize that you’re going to be repeating a chunk of HTML more than a couple times. This abstracts away the verbose-looking tree of HTML with Tailwind classes into a nice set of human-readable components.

“What happens if you need to change the color of that text everywhere?”

Spoiler alert: You probably never will need to change the color of that text everywhere.

But in the off chance that you do:

  • You’ve probably written a single, shared component. Change it there.
  • Consider using a custom shade or a custom Tailwind variable.
  • If all else fails, find-and-replace.

“This is not semantic HTML and CSS!”

There are lots of feelings on the internet about this. For a lot of folks — myself included — it was difficult to abandon the school of thought that CSS ought to be decoupled from HTML.

Adam Wathan, a co-creator of Tailwind, wrote about the “separation of concerns” with regard to utility-first frameworks.

At the end of the day, I think it’s important to remember that CSS class names should be useful to the developer, and I think Tailwind classes fit the bill.

“Why do you hate the cascade?”

I don’t. It’s pretty neat and powerful in a lot of cases.

But even in our complex web application, we haven’t needed to think about it at all. And when I return to other projects that leverage the cascade in different ways, I grimace when I have to spend time thinking about what is causing an element to be styled a certain way, hunt down the original definition, and override it with extra CSS properties.

“This smells like something back-end developers use just so they can avoid learning CSS.”

This is a common misconception: you still have to understand how to use CSS to effectively use Tailwind.

It’s not like Bootstrap, where you can copy and paste component classnames into your application and expect them to work.

With Tailwind, you still need to understand layout concerns, the box model, margin, padding, flexbox, floats, and CSS Grid. Tailwind just makes the concepts more approachable, since you’re not jumping between an HTML file and a CSS file.

“This is only for prototyping small projects, right? Surely you’re going to rewrite this with real CSS?”

It’s worked well for us so far, and we’re building a large, complex application with multiple engineers.

To this day, we haven’t written a single line of custom, “real CSS.”

How We Did It

I’ll start with some statistics:

  • In 21 days, we reached feature parity with what had been developed over the prior 12 months.
  • We had three full-time engineers, two engineering contractors, two designers, one project manager, one product manager, and one engineering manager.
  • We merged nearly 50 PRs and 500 commits.
  • We used the Next.js framework as a starting point; Apollo Client for GraphQL queries; Jest, React Testing Library and Cypress for testing, and Auth0 for authentication.
  • Several engineers have mentioned this is the happiest they’ve been at work in a long time.

Let’s dive into how we made this work, and why it’s worked so well:

Productive Code Reviews

As mentioned by one of our engineers in a retro, our code reviews have been really productive. This is impressive, considering we’re working with two contract engineers who literally joined the team during the 21 days, along with a full-time engineer who started at the beginning of this year.

At Vox Media, we promote a code review culture centered around kindness. Even with new engineers on our team, we all contributed code reviews with specific, actionable feedback written and interpreted with the best intent.

The speed of code reviews was impressive, too. If you had an open pull request, it was rare to have to wait for more than a day for someone to review it. This was crucial as we were building the base layers of the React rewrite. Delays due to code reviews could have easily halted our process or caused merge conflicts down the road.

Code Linting and Formatting

We formatted our code with Prettier and used a pre-commit hook to ensure each new contribution was formatted the same way. Imagine never having to argue about spacing, indention, or line length!

We also implemented the recommended set of ESLint rules which run on our CI platform.

The biggest win here was that it eliminated the need for “nitpick” comments in code reviews. I can’t tell you how much I loved this!

PropTypes and Testing

We opted to enforce PropType definitions with ESLint.

Initially, I was worried this would be a tedious chore for our engineers as we were trying to get the application rewritten.

However, it is a smooth experience — and it’s beneficial to be explicit about the required props for each component. As a bonus, we get automatic prop tables generated in our Storybook Docs!

In the Vue version of the app, we made the mistake of focusing too much on unit tests. As soon as we needed to overhaul the design or change the way a component integrated with a page, we had to throw the unit tests out the window.

This time, we wrote unit tests sparingly where it really mattered, and focused instead on end-to-end testing. We also didn’t focus on test coverage metrics or dogmatic test-driven development practices.

TypeScript

We did NOT use TypeScript. After experimenting with it in the Vue app, we quickly learned how much it slowed down our team and made it difficult to get onboarded into the codebase.

Using linter-enforced PropTypes was just enough for us. Someday, we may revisit TypeScript if we find it offers us huge benefits, especially in regards to GraphQL type safety as demonstrated by our friends building Coral.

Design Collaboration

Our engineers have a close working relationship with our design team. During this 21-day period, our lead designer opened a Slack channel specifically for showing off new design ideas, gathering feedback, and communicating changes.

Our designers use Figma which makes it really easy for engineers to collaborate and grab any SVG assets as needed. It’s also really easy to inspect the inner-workings of a Figma component and see what the CSS properties would be. While these don’t always translate directly to Tailwind classes, it’s useful to reference so we can find something close.

One of the questions we had at the beginning of this process was whether we needed to create a robust, component-based design system before we started building the application.

We did not, and I think that was the right decision. In order to iterate quickly on the React application, our engineers needed to be empowered to create new components quickly and efficiently.

Had we enforced a design system upfront, creating components would have been a much slower process. For example, engineers would often be blocked while trying to reconcile an existing component in the design system to fit in a new view or layout.

As our collection of React components matures, we might consolidate everything into a cohesive Concert design system, move it to a shared NPM package, and make it usable in other Concert applications.

Storybook

If you haven’t yet checked out Storybook, you should. It’s a terrific tool for rendering components in isolation.

This is particularly useful for creating and iterating on components without needing to set up their requisite state in your application. Imagine developing the “checkout” page of a shopping cart app. You wouldn’t want to have to add an item to your cart each time you refresh your page to see your changes.

Besides shortening the feedback loop, Storybook also provides really cool add-ons like Docs. This allows us to document each component as we build it, providing additional context as to how it’s used. Standard JS Doc comments and PropTypes are parsed automatically by Storybook, so we don’t have to maintain a separate folder for documentation.

Storybook also lets you define multiple renditions, or “stories,” of a given component in different states.

This helps promote design collaboration, as it’s much easier for an engineer to share a link in Slack to a specific component living in Storybook rather than try to describe how to get to it or fall back on a screenshot.

It’s Not All Sunshine and Rainbows

We’re only 21 days into this project, and we’re excited for it to debut later this year.

While it’s going super well so far, there have also been some growing pains:

  • We miss Vue classname support. It’s a bummer that we have to write custom helper functions for conditional classes and define explicit className props to be able to style child components. We’ve gotten used to it, but Vue handles this wonderfully.
  • Hooks can be confusing. Several members of our team have had to learn React for the first time. One thing we’ve noticed is that some usage of Hooks is not as clear or explicit as it was with class-based React components. For example, componentDidMount() is a really handy lifecycle method available in class components. In functional components, we have to leverage the useEffect(callback, []) with an empty array to mimic the mounting lifecycle hook. Additionally, other team members — myself included — have been perplexed by the behavior of the useEffect dependency array and what causes it to re-run multiple times. We’re still learning!
  • Finding up-to-date React training material is tricky, because lots of courses were recorded before Hooks were introduced.
  • There are unknown unknowns. Who knows what will come along in the future that makes us regret our decision.

Vox Media Still ❤️ Vue and Regular CSS

While the Concert Platforms team has recently adopted React and Tailwind for our application, we still use Vue across many apps in the Revenue and Chorus app ecosystem at Vox Media.

Vue is a powerful front-end framework with a healthy ecosystem and a great community. One of our fellow engineers, Michele, is a regular speaker on Vue at multiple conferences.

Some of our team members who don’t come from a JavaScript background enjoy using Vue because it doesn’t force you into the “everything is JavaScript” mentality. This allows them to be productive in Vue using a more familiar HTML and CSS template syntax.

At the end of the day, we’re firm believers that if you’re familiar with one modern front-end framework like Vue or React, you’ll be able to pick up and use a new one with relative ease because the concepts are the same.

If you’re someone who likes to learn new things, and you have experience with a modern front-end framework — heck, even if you don’t! — we’re hiring.


Special thanks to Messay Bekele and Rick Ross for their contributions and to Steve McKinney for thoughtful edits.