Architecting a React Migration to Fix The Things

This is really NOT a post about moving from Angular to React

Tristan Bhagwandin
Teachable
Published in
8 min readNov 27, 2019

--

Lots of companies are migrating from Angular to React. At Teachable, we’re no different.

There are a ton of reasons but in a nutshell, React is just a view library as opposed to a more complex MVC (model/view/controller) framework like Angular. It has less of a learning curve, is more popular with developers, and is closer to the javascript spec.

Since Q3 2018, we embarked upon a previously established path (see below) that would slowly but surely move us from Angular 2 to React. However, we realized that migrating the codebase this way was not going to solve our biggest problems.

The Old Way

Following in the footsteps of several predecessors (eg. https://hackernoon.com/how-to-migrate-an-application-from-angularjs-to-react-and-redux-de0e2d1f70aa), we chose this multi-step approach initially:

  1. Wrap React with Angular Router so that React Components can be used within Angular
  2. Migrate Angular Components to React Components
  3. Migrate Angular Controller logic to React Controller logic
  4. Migrate Angular Router to React Router
  5. Remove all traces of Angular

There’s really nothing wrong with this approach. It’s very stable to use Angular’s router to start and then “step by step” move parts over to React. This method avoids a page refresh and other complexities that come with running two completely separate apps and/or routers. One caveat however, is that the underlying architecture of the old app informs that of the new. Our underlying architecture was lacking to put it frankly.

Too Many Problems

  • Rampant code bloat especially for styles
  • Confusing, poorly understood Webpack configurations
  • Little to no component reuse between teams and no easy way to share code between different apps
  • Desktop first layouts despite a growing mobile user base
  • Inflexible layouts despite a growing international user base
  • In the worst case, a 23 second load time on our admin site (as per throttled Google Lighthouse mobile)
  • Poor/slow developer experience (no hot module reloading with Angular’s router wrapping React)
  • Little to no Accessibility consideration (unacceptable for any site that teaches others)
Teachable Legacy
High level overview of Teachable Legacy

Rearchitecting (Building the Foundation First)

Taking the above problems into account, we decided to abandon the old approach in favor of a foundational rewrite. We bit the bullet and decided that a page fresh (between the Angular and React apps in the interim) was necessary in moving towards a more solid platform. To prevent the past from repeating itself, we established some core standards for our new architecture.

Standards

Mobile because if it works well on a mobile device with a suboptimal internet connection, it’ll work well for the vast majority of our users. This tenet also improves app performance and increases productivity by avoiding the pitfalls of desktop-first layouts (code bloat, overrides). Our mobile usage is constantly growing and in other countries, it may be the only access available.

Flexible because it encourages apps to share code and handle variations in both design and content. For example, our marketing site has a different look/feel from the administrative side. To keep our components shareable, we need to support multiple themes. We also have a growing international cohort. To support different languages, our layouts need to handle varying lengths of copy, currencies, and right-to-left formatting (Arabic) without breaking.

Accessible because it’s the right thing to do. Period. Build everything with accessibility taken into consideration. (more on this in a future article)

Developer-friendly because a fast and enjoyable development experience enables the rapid delivery of product features to our customers.

Well-tested because fear of breaking current code hinders the innovation of new code. High test coverage minimizes this fear.

Mono-repo approach
Monorepo approach at the outset
Mono-repo approach complete
Monorepo approach upon completion

Tools

Scaffolding

Lerna Monorepo with Yarn Workspaces

As a base for our front-end we decided that using a monorepo architecture would give us the best developer experience and high-level architecture.

It’s cumbersome to maintain a growing number of repos when you need them to share common packages like a component library. For example, if we didn’t have a single repo and you wanted to update a shared button component, you would need to open the component library repo, commit to it, open all the other repos that use the button, commit to them… then WHEN YOU FIND A BUG on the button change you just made…boo hoo!

It’s easier to do it all in one place with a monorepo because instead of multiple repos, there are multiple packages in one repo with a single shared history. These can be modified separately or together in real time. They can also share dependencies and be published to NPM.

Building Blocks

Shared Component Library via Rollup

Rollup is a build tool similar to Webpack. However, it’s main purpose is for bundling Javascript modules. For our shared component library, we generate common and es modules for the benefit of Tree-shaking (dead-code elimination/smaller code) and external (legacy) repo consumption.

Rollup statically analyzes the code you’re importing, and will exclude anything that isn’t actually used. Although Webpack can do the same, Rollup does it with a very minimal amount of code and configuration. In our case we chose to leverage Babel 7 via rollup-plugin-babel for even fewer compilation steps. In the future we’ll post more about our library and learnings, especially regarding our components and usage of Storybook.

Babel 7 + Typescript

At Teachable, we use Typescript for its typesafe checks, inherent documentation, and as general bug spray. With the release of Babel 7, Babel itself can handle Typescript compilation along with its own Node/browser support. Before that, we’d have to leverage separate loaders in a multi-step compilation process.

We still use Typescript to generate our declaration files though. We need declaration files for the build output (at that point it’s compiled Javascript and not Typescript) so typings are fully available for the package consumer’s benefit.

Shared Style System

As a basis for our component styles we decided to use what most of our engineers were comfortable with namely, CSS Modules. The great thing about CSS Modules is that it mitigates CSS scoping between files, allows for CSS className mangling, and can share some CSS (Sass) variables with Javascript. Through a system of class utils, we have our spacing, flexbox layouts, and typography patterned. (we’ll touch more on that in a later post)

Long strings of classNames are the main drawback to this approach but when the time comes to move our layouts to a CSS Grid or some other new spec, find/replace is fairly simple. Another drawback is handling Sass variables, which are overridden on the server and cumbersome to compile. To mitigate some of this we avoid overriding variables and bundle them as _.scss files in our public folder for external use.

Builder

React Scripts (eg. Webpack)

Perhaps you’ve heard of Create React App? If you haven’t, it’s a thing that spins up a brand new React App for you by leveraging React Scripts. In essence, React Scripts abstracts away almost all of the React Webpack configurations so you don’t have to deal with them. If you prefer your own custom Webpack setup, you simply “eject” out of React Scripts and from that point forward, maintain your own Webpack configuration.

The question of whether to “eject” or to “not-eject” is one that has stirred much debate on the interwebs. On the one hand, if you never eject, it becomes fairly trivial to keep your React and Webpack configurations up to date. You simply update the version of React Scripts. However, React Scripts includes code you may not need and offers limited customization based upon their assumptions of common usage.

Previously bitten by poorly maintained and hard to decipher Angular/React Webpack configs, we chose to stay in React Scripts and temporarily fork the create-react-app repo for any missing controls. With the addition of Babel 7, Typescript, Jest to React Scripts (thanks React team!), it made the most sense to have React keep their Webpack configs updated instead of us.

Foundation: MonoRepo

With the foundation complete, we gave it the very unexciting name MonoRepo, dubbed our exportable component library TeachableUI, and spun up a new CRA (Create React App) called MonoAdmin for our admin app. We also set up server-side routing between the old and new admin app (not trivial and deserves an article unto itself).

The following benefits occurred with MonoRepo taking its first steps into production.

Results (Post Foundation and Follow Ups):

  • Fewer duplicate styles via shared classes
  • Shared component library consumed company wide
  • Standards known and established (mobile, flexible, accessible, developer-friendly)
  • 4 second load time (400% + speed boost) on admin as opposed to 23 (as per throttled Google Lighthouse mobile)
  • it’s only a few pages for now but we should never run into a noticeably slower loading app like before, no matter the pages we add — this is because of dynamic import support, shared components, and the ability to spin up multiple apps. This time should get much better as we improve (including better api/endpoint handling, Server-Side Rendering, CSS Critical Rendering).
  • Improved Developer experience
  • Improved Accessibility Support (especially screen readers)

Wrap Up

The fact that MonoRepo even happened is testament to Teachable’s culture of experimentation. We took some risks, learned from our mistakes, and fixed the things.

Overall productivity has increased and developers are enjoying the codebase. Teachable-UI is morphing into a full design style guide (thanks Storybook team) and its usage/breadth continues to grow.

However, the Angular to React migration has become an opportunistic effort as we attempt to balance it with the creation of new product features. Here’s the rub:

MonoRepo’s major benefits are not incremental and require a full migration. A full migration requires a lengthy halt to product creation… but product creation is faster than ever on MonoRepo. Balance both and both slow down from context switching and rising tech debt.

Regardless, it’s comforting to know that at Teachable we have a solid foundation to stand on.

Have you dealt with similar migration woes? Strong opinions on monorepos? Maybe you’d just like to see some code pertaining to something in this article? Comment below and let us know!

--

--