Editor’s note: This article was originally published in November 2016, and was re-posted in February 2021.
We are in the process of migrating our front end from server rendered views with jQuery and Knockout (KO) sprinkled on top to TypeScript and React. It's been a few months now since we started going down this path and we've shipped some big updates in our new stack. We've learned a lot and are excited to share our experience adopting this tech because we know it's something a lot of companies are evaluating right now.
As our UX requirements have evolved to accommodate more powerful and complex workflows we also noticed it was becoming increasingly difficult to maintain and add features in our UI. This is the pain of progress in any application, but we were motivated to investigate new options for our front end for a handful of reasons.
- We were dealing with a class of problems that are a result of “sprinkling in“ KO + jQuery that we thought could be avoided by using a modern UI library
- Our goal is to move toward a component-based UI and KO's component system was lacking some key features, like rendering lifecycle hooks
- Our front end testing tooling wasn't where we wanted it to be and we knew there were better options
Before considering specific technologies, we laid out the follow goals:
- Easy to understand and ramp up new developers
- Increase developer productivity
- Encapsulate component code
- Sane, scalable CSS architecture
- First-class feature toggle and experimentation tools
- Support a well-documented, component-based design system
Equally important to what we defined as goals for the transition, we also laid out the following non-goals just to be clear what we did not want to optimize for:
- Performance - Although performance is important to us, we weren't hitting a bottleneck with KO here. Based on the benchmarks we saw it looked like any of the popular frameworks would provide the performance we need.
- Using the latest and greatest tech - While we wanted a modern stack, we also didn't want to chase new and shiny tech. We wanted something that had already been battle tested with great supporting tooling.
- Productivity on multiple clients - Although we are seriously considering React Native for our iOS and Android applications (and are hiring a mobile developer!), we wanted to choose the best tool for the job and not worry about the development experience on other platforms yet.
We knew the key to a successful migration process would be to do it incrementally. We wanted quick, consistent wins to build momentum, so we broke the process out into 5 main steps.
Step 1: render a component
The first thing we did was set up the necessary tooling to render React components alongside our existing KO code. We evaluated a few different methods of integrating React into our Rails app. There are some options like the react-rails gem which would let us quickly integrate everything with the asset pipeline, but we knew that with any modern front end framework we would have access to another level of tools only available to us if we used a true module bundler.
We looked at Browserify and webpack, which both appeared to be solid options, but ultimately settled on using webpack because of the community and great ecosystem developing around it. This gave us the flexibility to use any front end tech we wanted and deliver the built assets to the Asset Pipeline.
Now that we had everything configured we built out our first component. This was just a hello world type of component only visible to internal users at Handshake. Our goal was simply to get something out to production as quickly as possible using the new tech.
Step 2: write a feature
The next step was creating an actual feature in React. We decided to implement a new student onboarding flow. We thought this was a good candidate to create something that provided a lot of value to our users and served as a good trial for our evaluation.
It’s important to note that in this stage of the process we didn’t focus any effort on making our development environment perfect. Aside from a couple third-party components, we intentionally avoided spending a lot of time perfecting our webpack configuration and setting up additional libraries we knew we would want to use at some point, like Redux.
We also relied on our existing testing framework in Rails for request specs rather than setting up new front end testing libraries. This enabled us to get the feature out on a fairly aggressive timeline even though we were introducing new technology along with it.
Step 3: going all in
We considered the implementation of student onboarding a success and wanted to dive deeper into the React ecosystem to see what the development experience would be like if we set up all the supporting tools we would require for day-to-day development. We looked for a feature we thought would be a good fit for testing the full capabilities of our new stack and ultimately settled on a redesign of student profiles, one of our highest traffic pages.
In addition to implementing a lot of new functionality we also wanted to spend some time improving our React development experience. We spent time hardening our linting rules, setting up Redux, discussing and establishing conventions, and setting up our front end testing libraries. We ended up being very happy with the development experience we were able to create.
The new profiles contain a lot of different sections which were well-suited for React's component-based architecture. This allowed us to encapsulate each piece of the profile and enable multiple engineers to work on different pieces of the profile without stepping on each other's toes.
In addition to having a good development experience, the final product of the new student profiles is something we have received a lot of positive feedback on. This was enough for us to commit to making React a part of our stack for the long haul.
Step 4: empowering the team
Now that our new stack was at a point we were confident everyone could have a good experience working in it, we needed to make sure everyone on the team had the knowledge and resources to be productive. Only a handful of us had the opportunity to go through the process of setting up the environment and there were gaps we needed to fill in knowledge about the new tech.
We've been doing a lot to share knowledge across the team, but this is where we are at today and are still making progress on this. Our focus is on empowering the team through things like:
- Documenting patterns and best practices
- Creating code generators that make it easy to create new components that follow best practices
- Creating a living style guide of common components to enable the design and dev team to speak the same UI language
- Doing deep dives into specific parts of the stack
- Publishing regular release notes on changes in the front end
Step 5: continuing the transition
This process started out with just a handful of developers experimenting in some new technology, but we are at a point where the team is starting to feel comfortable in it which enables us to be more aggressive about migrating existing functionality. Most of our app is still in KO and will be for the foreseeable future, but we will continue to move things over as we implement new features across the app.
Although these changes are happening incrementally, we know there is still a lot of risk in migrating critical portions of our UI. We try to eliminate as much risk as possible from making these types of changes by rolling out updates alongside the existing UI and only enabling it for a small subset of our users. As our confidence grows that things are working the way we want them to we continue rolling it out to more of our users.
This provides a great workflow for developers because we can ship code immediately to production, enable it for internal or beta users, gather feedback, iterate, and continue to roll out to more users only when we are satisfied with the results we are seeing.
We've learned a lot in this transition, but there are some important takeaways that we want to share because we feel they are applicable to any team implementing new technology while trying to maintain productivity.
Focus on small, consistent wins
Shipping things consistently builds momentum. If you aren't pushing changes early and often it will be extremely difficult to successfully adopt new technology, or it will be done at a great cost to productivity and morale. This is the reason doing full rewrites is a bad idea.
Move fast and break nothing
Although we agree new functionality needs to be shipped early and often, we don't think you need to sacrifice stability to move quickly. You can manage risk by breaking down large projects into small units of work that can be deployed early and incrementally rolled out to your users. Zach Holman also has some great thoughts on this topic (which is where we stole this phrase from).
Learn out loud
It's never too early to start sharing what you are learning. One of the biggest challenges for us has been empowering everyone on the team to be productive in the new stack. Making a concerted effort to share knowledge and train your team is more important than the technology you select.
Our vision is to have a first-class UI development experience and we are excited about the tools we have at our disposal in the React ecosystem to build it. With React Native we even have the opportunity to create a consistent experience across all of our clients.