It’s no great surprise that replacing your UI framework is a large job. Rewriting hundreds of views takes time, coordination, and determination. But with the right techniques, you can topple this Goliath-sized task.
At Retail Zipline, we set out to replace and consolidate our existing collection of renegade UI frameworks. Using these processes, we migrated 650 views with a core team of three in under two months.
Obviously, replacing your UI framework is only a worthy use of developer time if it aligns with company goals. Here’s what you need to ask first:
Picking a clear goal will shape the project and provide guidance on what can be cut. You might personally be dying to improve the UX of your uploaders or WYSIWYG editor, but if that’s not required by the framework, leave it for another project. Furthermore, if it is required by the framework, I recommend upgrading those smaller projects separately so the framework project is focused.
We considered the upgrade technical debt. The developer team was the benefactor, so we sought to maintain the same visual design as much as possible. That meant we didn’t add new functionality, didn’t fix existing UI bugs, and didn’t change page hierarchy. Our users would hardly notice a change at all.
At the time, we had three separate UI frameworks: Bootstrap 3 from when the app was originally built; several custom-built BEM-style components; and custom utility classes, like Tailwind CSS.
Building backend features was straightforward, but progress was halted by design decisions. The lack of patterns implicitly encouraged us to write new designs for every feature. Replacing the framework wouldn’t solve mismatched patterns, but it would bring all views to the same baseline and provide pattern options.
There’s no such thing as The Perfect UI Framework™ — rather, pick the one that meets the demands of the project and fits with how your team gets work done. The last thing you want to do is waste time fighting a framework because everyone on the team finds it awkward.
We picked Bootstrap 4 because we need to support IE 11; don’t want to create custom components from scratch; and have a small frontend team that doesn’t want to become a design bottleneck. Regardless of your framework choice, the approach we took will help.
Once goals are in place, break down the work into smaller projects. What can be excluded entirely? What can be released in smaller batches? It might be possible to replace the framework in stages so you can ship it faster. In a Rails monolith, a natural break could be in asset bundles; in microservices, each service. An area specific to a particular user archetype may be another cut.
Our application is a majestic monolith, with a couple supporting services, like a Mailer and iFrame widget, and four distinct areas based on user types. We excluded the services entirely because they use separate asset bundles. Then, split releases by the distinct sections. We also excluded our admin area from the initial scope — 170 views in itself.
We refined the goal to upgrade each customer-facing area as an independent release, and the remaining views after. Thinking about work orthogonally and dividing releases by their dependencies helps ship faster.
Major frameworks are often incompatible with competitors and even previous versions. Make your life easier by considering your new UI Version 2 of your app instead of something that can coexist with the old UI. Our collection of UI frameworks was born of failed attempts to replace what was there before, bit by bit. It’s not impossible to do, but the work-in-progress state is slow and demoralizing.
We created an additional views_v2
folder where all the upgraded views lived, a view resolver that rendered the new view and fell back to views
, and v2
CSS and JavaScript bundles.
If you have several bundles, like we did, separate only what’s necessary. Our vendor
bundles imported Bootstrap 3, so we created vendor_v2
importing Bootstrap 4, while the same application
bundle was imported in both views
and views_v2
. If your views are written and rendered in JavaScript, you likely won’t need a separate view folder, but you’ll want to include them in your separate asset bundle.
With the setup in place, we could now start building new views in what felt like a brand-new environment. Building from scratch is easier, right? Maybe not.
Manually rewriting every single view is a grueling, tedious process. It quickly became apparent we’d spend the rest of our lives on this upgrade if we didn’t start automating the process, so we wrote a small tooling library to speed it up.
Checking the status of remaining work is important for estimating, planning, and completion. We created bs4_migration_check
to report the remaining views by section. It meant we got a better idea of how long a section might take and could ensure nothing was missed or forgotten.
Since UI frameworks are mostly built with CSS classes, you can automate the name replacement. This transforms the job to editing instead of writing from scratch, which is much easier. We created two tools, bs4_start
and bs4_ugrade
, to be used together.
The first copied a given subfolder of work into views_v2
and committed the files. This set a baseline to review what lived in views
.
The second used find and replace to change all the class names that were easy switches, and alerted those that needed more attention. For example, we had previously used .flex
to make something a flexbox container, whereas Bootstrap uses .d-flex
. From there, the job was an editing process, and we only had to fix bugs.
Feature flags ensure nothing is customer-impacting until it’s ready. We feature-flagged all our work and merged into master as quickly as possible so other teams could work on the newly committed views, and we could avoid large merge conflicts at the end of the project.
The view resolver enabled new views on a per-action or per-controller basis, and was held back from customers with a Launch Darkly feature flag.
In JavaScript, we created a global variable, window.CONFIG.bs4
, for upgrading API calls in libraries that changed. For example, Bootstrap 3 uses destroy
to clean up events, whereas Bootstrap 4 uses dispose
. Using the flag in specific JavaScript files meant we could mostly use the same bundles across the two app versions.
While it might seem useful to include some automated screenshot testing, we found the diffs were so big it ultimately wasn’t worth the hassle. Instead, we manually took screenshots, which doubled as a first pass at QA.
When tackling a huge overhaul like this, you want to make the process as easy to replicate as possible. Write documentation on the tooling so anyone can use it. Save the UI patterns that emerge for later reference and future consistency. Record your setup, which doubles as a tear-down guide for when the project is complete.
The right tooling will have a dramatic effect on the completion of the project. We estimate our project would have taken 3–4x longer if not for bs4_upgrade
alone.
Tooling is only one piece of the puzzle. Your team working conventions are nearly as critical. And while tooling is easy to enforce in code, working conventions need to be agreed on by the team.
Keep the team working together on the same release. Once a week, set the team focus and create the week’s goals. Setting goals on Mondays meant we planned our work in roughly 30 minutes, then got to it. The short time frame and focus meant we didn’t need daily stand-ups, manager check-ins, or other distractions.
Communicate who’s taking what so you don’t step on toes, and ensure it’s accurately reflected in your project management tool. We used Basecamp, assigned ourselves tasks we actively worked on, and added due dates for the end of the week we expected to have it complete.
Define a “getting help” agreement so no one feels awkward when they’re stuck. We decided to spend no longer than 20 minutes on a given piece of work before reaching out to pair with someone. This also helped create consistency in patterns.
Now you’re ready to work through it. Start small, in a self-contained area, to get familiar with the framework. You’ll replicate what worked for each area and adapt as the team continues forward.
In our planning sessions, we broke down who was responsible for what and set a date for a QA bash toward the end of the week. We made migrating the views a priority and intentionally chose to defer fixing some bugs until then to prevent context switching.
The entire team swarmed on each release and had it fully complete before moving on. It was great for team morale and made progress really clear.
We kept PRs highly focused. Generally, we migrated a model’s entire folder in a single PR — index, show, new, edit. It reduced the repetitive overhead of creating new branches while keeping an area isolated. PRs split on gut feelings rather than science — if it felt too big, it got its own PR.
Since version 2 is your new codebase, take the opportunity to organize it as you see fit. Rename, move, or remove folders and partials. Rewriting every view is a good time for housecleaning, provided you keep track of what changed so you know you haven’t missed anything in the final sweep.
Expect framework conventions to emerge and change, so scope time to go back and consolidate them. There might be a couple ways to build sub-navigation, but your team’s favorite will surface during the project. Consolidation goes quickly and should be the last thing to do before considering a release done. We did it after fixing bugs from the QA bash, so it often became the last PR of the release.
And finally, once you’ve completed the entire migration, remove your old views and tooling. This should be the inverse of your setup.
I’ve upgraded UI frameworks several times in my career and have found these processes to be the most effective for getting the job done. Upgrading piecemeal works, but you never really get that new, fresh feeling because the work in progress is so long-lived. In contrast, the version 2 approach means the project can fully wrap up. And there’s no better feeling than when a project is done done.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.