Backbone refactoring adventures
July 22, 2023
Over the last 3 months, I have spent a lot of time working on ArangoDB's open-source codebase, which was written in 2014 in Backbone + jQuery.
There are a lot of things in this UI that needed to be worked upon, but to do any of it was a big task - having to work with the DOM directly without making sure nothing else breaks, which is quite a difficult thing to do in Backbone.
Thankfully, some effort was already in place to move some parts of this to React, so there was a way to inject React into the DOM and work with it. As we started to move more of these, we found the natural need for a good component library with a consistent design system in place.
Reasons to choose Chakra
Having worked with Chakra before, I knew it solved a lot of composability & flexibility issues, and was a good design system to have. The other end of this spectrum is the earlier system I have worked with - semantic-ui, react-toolbox, and to some extent, MUI (v3-4)
Some of these don't exist anymore (toolbox), and others like MUI have improved their game a lot (from what I hear).
I selected Chakra because it came with built-in support for things like the
as prop (with great Typescript support too), which allows a lot of possibilities. For example, using a Button as a link, or using external Icons with internal props, becomes very much possible and even very nice to use.
Chakra is a great base to start your own library (by wrapping around Chakra), as all its own components are made with the base level
<Box> component. This whole system is based on
styled-system & then
theme-ui, which propose this primitive
<Box> component as a building block. Rebass was an early implementation of this.
This system also provides you with a utility CSS experience, without having to pass classNames (using utility CSS props like
A con of Chakra is the lack of static extraction (styles are injected using JS at runtime, using emotion), but I eventually hope to upgrade to the v3 which solve this (according to Chakra's roadmap/twitter, they are working on static extraction).
Another potential con is the lack of familiarity - which makes it a little hard to get started - but over time this gives you superpowers and can make you very fast with the code.
The issues with routing
As mentioned, there was already a way to inject React into a backbone page. This works by using Backbone.Router, where we extend it, and ask it run a function when we hit a route, like:
/route: renderRoute. It's right here that we injected React using
ReactDOM.render, and made it a separate route.
This was very convenient, but over time it opened a few questions:
- If we want to change only a part of the page - is that possible?
- If we want to control the routing - for example, we don't want to allow navigation away if there are unsaved changes - is that possible without a full overhaul?
For #1 - For complex screens, the answer invariably was to port the whole thing to React at once, a big undertaking. For simpler cases, my colleague Alan figured out a way - which was injecting the Backbone render code into the DOM in a
useEffect & jQuery. This was a temporary stop-gap, but eventually, we want to remove these dependencies.
For #2 - window.onunload worked well for a page refresh, but on clicking on a link in the page, Backbone took over & navigated us away. In the react-router world, this can be solved by using a
<NavigationPrompt>, but here we needed to patch the Backbone history API. We created a hook -
usePatchNaviagation, which did this as a side-effect.
One of the problems with this routing was that it was hash-based (which was the case for most SPAs). React router also has hash routing support, but it's partially broken when using the
Link component due to historical reasons. A path there is to migrate to router v5+, which is on my to-do list.
The CSS reset & specificity issues
Now, this existing codebase has a bunch of styling solutions - like bootstarp, pure-css etc. And while some of these are harmless, others come with their own CSS reset, which make some very opinionated changes to all elements.
This was a hidden problem, which came to light when we added Chakra UI - whose style got overridden due to the higher specificity of other styles. This in turn, lead to broken UI, and Chakra's props not working in some cases.
Thankfully these were few enough to un-reset manually, and using a hook to inject them into the page when a React root mounts (& removing on unmount). This got abstracted to a hook -
useStyleReset. While this wasn't a perfect solution, it allowed us to move faster.
Some open issues
There are still a lot of open questions in this big refactor - and most of them can only be solved when we flip the routing to React. This can happen once we move the majority of complex screens to react first, to ensure that nothing breaks. Login/sessions are a big part of it, and so are the various screens which allow data input. Doing this without causing any regression, is almost impossible without intense manual testing.
Still a work in progress
The primary goal with this refactor was to create the ability to ship good quality software, fast(er).
I think we have partly achieved this goal in the React part, but there is still a long path ahead.
One of the other goals is to allow straightforward reasoning about most of the code - by removing the patch-y stuff and using a very composable, consistent paradigm.
The component API can sometimes get too bloated, which points to a need to do tiny refactors. This is a continuous process, and it's natural to keep going between the state of 'a great API' to 'bloated, needs refactoring'.
The idea of continuous cleanup really helps, and if used well, Typescript & the tooling around it lets you do it with confidence.