BugHerd has survived for nine years as a successful SaaS product built using Backbone, jQuery and Rails. Over that time, it's been relatively easy to ignore new technology in favour of stability (and there was a period of inactive development).
However, it became apparent we needed to start thinking about updating the technical implementation of the front-end, because:
- Growing the team - There are more React devs for hire (in Melbourne, Australia); I mention Backbone and the reaction is mostly familiarity with the name, not the library itself - let alone a desire to work with it.
- Onboarding Developers - Like other small SaaS companies, it's common for long-standing employees to end up with domain knowledge, and when they go, the knowledge goes too. It can be re-learnt, but often in a 9-year-old codebase that's riddled with complications, it's costly and can be off-putting. We will prevent domain knowledge as much as possible, but having a modern codebase that's easy to digest could be just as important.
- Simplicity - If you've ever used Backbone, you know why React is good, and therefore know why our views can be very complicated. If we're investing time in improving Backbone views, we could hypothetically implement something like React in a similar time.
- Growth - We want a culture which encourages learning. We have a "junior" developer in the team, and our codebase has suffered a well-known fate - in a race to profitability and customer happiness, code quality can be sacrificed. It's a fine line, perhaps sometimes it was wrong, but largely it comes down to a business decision. That said, the 'do what I do' mentality is something I want to be proud of, and looking at the codebase as an example of implementation is something I want all of our developers to do.
The above is to say we didn't take the investigation lightly, as "rewrite" horror stories are very common, and as a team, we'd be burnt by something very similar before. After a lengthy analysis about the technical implementation of moving from Backbone and Embedded Ruby (ERB) to React, we decided to bite the bullet with a few hard rules:
- Piecemeal - This is sometimes referred to as the Strangler Pattern. The general premise is you write a new system around an old one until it's that prominent that the old implementation is strangulated. We would need to be able to write new features in React, but also rewrite old features to React without letting the new code affect existing implementations and making rewrites lengthy.
- As required - There are two general objectives at play here, modernise the codebase and improve product development time. If an existing feature was changing drastically or was getting extended with new responsibilities, a rewrite was obvious. If a part of the old frontend needed some copy or colour changes, a link added etc. a rewrite is not necessary. There is no clear line or requirements which dictate if something would be updated to React, but it's always treated as an important discussion.
- Frontend-only - While not ideal, the work to replace Backbone and ERB templates with React should not affect the backend. This is to avoid scope creep and ensure we can release updates with confidence in timelines and process; the way data travels to and from the backend shouldn't change, so the risk can be minimised.
- Take time - There's no illusion that this is a monumental task and it will take time. It's okay if there's large, critical parts of the application in Backbone now, months and perhaps even a year or two in the future. Above all our commitment and priority are the customer and their happiness.
So with confidence in the fundamentals as to why and philosophically how we'd proceed, we laid out the best technical way to meet our requirements and expectations? This isn't a new problem, but there were a few core questions that we had to answer ourselves.
ERB Templates
Webpacker is the gold standard in using Webpack in the Rails toolchain to compile JavaScript modules. While there are some other options, Webpacker is the most widely accepted and supported.
The installation of Webpacker is very trivial and it's easy to get started. The first thing you'll notice is for every feature you develop, it's a different entry point for Webpack and therefore a different app. This has two huge drawbacks; build time for developers and load time for end users. Initially, we threw all of our global dependencies (react, react-router, and etc.) in a single pack and referenced it before individual packs. It's like half baked manual code-splitting and therefore isn't a great solution. A better idea is to end up with a single app (a single CSS file and a single js file and then perhaps, in the future, rely on Webpacks code splitting):
Above we have a global pack which contains two apps (components, really). propListenerRender has some extra functionality attached (explored below) but in this simplistic example, all it does is checks to see if a DOM node (an element with an id of host, after the DOM is ready) exists, and mount the app if it does. This means we can serve all of our features at once but make sure they're not mounted if they're not required.
Webpacker says to include your pack tag and stylesheet tag wherever you want to use the specific pack. That means we'd be writing something like this everywhere:
That said because we're now including everything in a global pack, the StyleSheet and JavaScript packs can be included in your head and then you can render just the element wherever you want a specific React app.
What about data?
The biggest concern was how to cleanly access data the old ERB templates used. We had a commitment not to introduce changes on the backend; we couldn't write new endpoints or drastically change existing ones. ERB templates are great, it's literally embedded Ruby which means templates aren't driven by typical RESTful APIs but makes getting data into a React like environment atypical.
In an ideal world, you use a store like MobX, it gets hydrated from a few API calls and you have a nice bucket of data inside your React application that you can build views with. Our real-world scenario was we needed to push data into our React apps. To do this, we rely on setting global JavaScript variables in our HTML page which are then read in React. We tried a few alternatives, like using custom (or data) attributes on the container element, but syntax and encoding was a killer.
Above you can see a partial we use to render containers for mounting points and how the partial is used. The name is used for building the global JavaScript variable and the props is an object of properties (the value of that variable) you want React to intercept. A key focus here is to make sure you escape </ as it stops custom data (think user comments, other editable fields) closing your script tag and causing malice.
To read these props in your React app, look for the correctly named global variable and parse it. Above I mentioned propListenerRender containing some helpers, and parsing the props and passing it to our App is one of them:
Two things happen in the above file, `getProps` finds a global variable and parses it (ensuring to unescape closing script tags that we escaped above) and the default function waits for the DOM to be ready and then mounts the app to the host.
With the above, it should be relatively easy to get started with using React in your Rails environment 🍦.
Backbone Templates
A large portion of our application is very dynamic for our end user (think kanban board) so we also have a bunch of Backbone templates. If you haven't read about using global packs above, you should - the same concept is a foundation here.
Backbone is easy to slowly transition to React for two reasons: it's JavaScript and more than likely, you're already using it for a bunch of data management. We made a specific tradeoff to make our transition "away" from Backbone easier - move the rendering into React and keep the data in Backbone. This means we keep Backbone around for a while as a data layer, but the drawbacks are removed if the view is entirely in React.
Similarly to how we pushed data into our React apps for our ERB templates, we didn't want to add backend work for updates to our Backbone ecosystem. The way our Backbone templates work, a lot of the data is present for the view without a request - it's baked in via an ERB template near the top of the tree. If you're in a similar environment, here's how we tackled it:
How do we mount the React app in the Backbone render?
Using ReactDOM.render is trivial in a Webpack application because you've probably imported it at the top of the file. However, if we want to use ReactDOM in our Backbone templates, it needs to be globally available. In your global pack, add:
And in your config/webpack/environment.js configuration file (read more about them here), make sure you can bind your application to the window:
Now, your entire application (including non-Webpack bundles) will be able to access window.ReactDOM.render:
While the above can look specific to task or bugherd the main concepts are: it's externally controlled, the getReactProps function and how the React app is rendered.
Backbone generally is a labyrinth of views calling other views, and our core application is no different. Here, task.create.js is managed by a core application.js. Whenever a user changes a task or a similar event occurs, setTask will be called and the task inside our Backbone template will be updated.
To build a nice properties object, getReactProps is called. If there's no task, we just return an object with a loading key. This is a pretty nifty space where you can do all sorts of state analysis and management external to your React app. The last step is then to render the React app in the Backbone render function.
What about two-way communication between Backbone and React?
We've seen that sending data from Backbone to React is trivial. What happens if React is mutating data (perhaps adding assignees to a task) and we want to tell Backbone? This is of particular importance if that data is used elsewhere in your application and you don't want to force a refresh. If you can't communicate both ways, your core application is stale and your React app gets out of sync.
There's a simple solution though, you can pass a function to your React component which can get called internally:
I hope the above has given you insight into how you can start to replace parts of your Rails and Backbone application with React. We've been working with the above methods for the last few months and some things are clear:
- React is really a much nicer way to render a view than Backbone is, the amount of complexity we've removed from our Backbone templates is something we're very happy with.
- It takes grit to ignore common best practices (like a store hydrating from an API) and power through.
- It's easier to do time and complexity estimates with new React features than it is to estimate wrangling Backbone.
- Being able to say React is a part of our ecosystem has definitely changed attitudes towards those we approach to do some work with us 🧠...