We refactored 10K lines of code in our open-source React project

Sidney Alcantara - Feb 9 '21 - - Dev Community

Know the how, the when, and the why behind our refactoring

When working on any project, especially in the MVP stage, we as developers often prioritise one thing above all else when writing code: making sure it works. Unfortunately, this can mean we write code hyperfocused on the MVP’s requirements, so we end up with code that is hard to maintain or cumbersome to expand. Of course, this isn’t a problem one can easily avoid since we don’t live in an ideal world. The forces of time are always against us — sometimes we just need to push something out.

I’m a software engineer building Rowy, an open-source React app that combines a spreadsheet UI with Firestore and Firebase’s full power. We ran into this exact issue with some fundamental code: the code for all the different field types, from the simple ShortText to the complex ConnectTable field.

After refactoring, we now have a more solid foundation to build more features, we squashed a few hard-to-find bugs, and we now even a guide on how our contributors can write new field types.

When code smells and tech debt became big problems

When we first started building Rowy, the idea was to build a spreadsheet interface, and naturally, the resulting product closely matched that. Looking at old screenshots, it’s remarkable how closely it resembles spreadsheet programs like Excel and Google Sheets:

An old screenshot of Rowy with a UI very similar to a spreadsheet program

A screenshot of an old build from February 2020

We used React Data Grid to implement this. It accepts “formatter” components used to render cells and “editor” components used to edit cells when a user double-clicks the cell. We structured our code around this, with formatters and editors becoming folders alongside the code for Table.

A few months later, we added the SideDrawer, a form-like UI that slides over the main table. It was designed to make it easier to edit all the fields of a single row at a time, which we found was an everyday workflow for our users. At the time, it seemed like the most logical way to structure this new code was similar to how we structured the Table, so we created a Fields folder in the SideDrawer folder.

Screen recording of a user selecting a cell and opening the side drawer

But as we maintained this code, cracks began to show.

One of our distinctive field types is Action, which displays a button on the table that lets the user run code based on the row’s data using Firebase Cloud Functions and show the results in the very same cell. We’ve used it for novel applications such as setting our database’s access controls from right within Rowy using Firebase Auth custom roles.

We had a bug where the Cloud Function wasn’t receiving the right parameters when called by Action cells. But to update the code, we had to do it in two separate locations — the Table formatter and the SideDrawer field. Not only that, it turns out we had duplicated the code calling the Cloud Function due to time constraints. There was simply no clear location for that shared code, and the bug was too high-priority for us to have time to answer that question correctly.

The final straw was when we noticed we had inconsistently implemented the column lock feature. Some fields remained editable in the SideDrawer but not the Table or vice versa, or we didn’t implement it at all for that field. This was a result of adding this feature after we had implemented the minimum requirements for each field type, so we had to go through each Table formatter and each SideDrawer field — double the number of field types we had. This tedious manual process was clearly prone to errors.

At this point, we knew it was time to refactor.

Refactoring for success

We identified the main problem: we didn’t have a single place to store code for each field type. It was scattered throughout the codebase: Table formatters and editors, SideDrawer fields, column settings, and more. This scattering rapidly inflated the cost for adding new features for field types and weeding out bugs.

The first thing we did was invert our approach to code structure entirely — instead of grouping code by each feature that would use the field types, we grouped the code by the field types themselves.

Diagram visualising the previous paragraph

The new approach translates to a new top-level component folder called fields, comprising subfolders for each field type, and within each, we have files such as TableCell.tsx and SideDrawerField.tsx. Then we could export these features in a config object, so all this code would only need to be imported once by the consumer. This is similar to a problem solved by React Hooks: grouping related code and not having to think about lifecycle methods.

This approach also simplifies how we import a field’s code throughout the codebase. Previously in the Table and SideDrawer, we would rely on switch statements that looped through each field type until we could fetch the correct component and import each field one by one. So whenever we added a new field type, we would also have to add a new entry to these switch blocks — again ballooning the cost of development. Instead, we could create a single array with every field config, then share it across the codebase. So we only need to define a new field type once.

Additionally, the config object lets us quickly implement new features and ensure all fields do so correctly. Now we could simply check if a field’s config has a property. And since we’re using TypeScript, each config object must implement our interface, which can enforce certain features (properties of the interface) to be of a particular type, such as a React component accepting specific props. This new functionality allowed us to fix column locking implementation and made it much easier to develop a new feature, default values for columns. All we had to do was add a new property to the interface.

Example of the significant amount of code we were able to reduce with the new code structure

With this in mind, our refactor not only made our code easier to maintain and fix bugs — but it also provided a much more solid foundation on which we can build advanced features for fields and removing extra costs to development.

Lessons for the future

Of course, we could have avoided all this pain and extra work had we initially gone with this approach. But we don’t live in an ideal world. All of the non-ideal solutions I mentioned above were the result of time constraints on our end, especially when we were working on other projects simultaneously, which directly impacted day-to-day work.

Many of us work for a business that doesn’t have excellent code quality as its primary goal. As developers, we are hired to build tech solutions that meet business requirements, and the “how” is abstracted away. In this case, however, our poorly-structured code and the amount of accrued tech debt were directly impacting our ability to work.

And whilst writing this article, I came across Refactoring.Guru, an excellent guide on refactoring. We clearly satisfied their first recommendation on when to refactor: “When you’re doing something for the third time, start refactoring.”

This experience has taught us many valuable lessons on code structure and when a refactor is necessary. I hope you’ve gained some insights by reading about our journey.


Thanks for reading! You can find out more about Rowy below and follow me on Twitter @nots_dney.

GitHub logo rowyio / rowy

Low-code backend platform. Manage database on spreadsheet-like UI and build cloud functions workflows in JS/TS, all in your browser.

✨ Airtable-like UI for managing database ✨ Build any automation, with or without code ✨

Connect to your database and create Cloud Functions in low-code - without leaving your browser.
Focus on building your apps Low-code for Firebase and Google Cloud

Live Demo 🛝

💥 Explore Rowy on live demo playground 💥

Features ✨

20211004-RowyWebsite.mp4

Powerful spreadsheet interface for Firestore

  • CMS for Firestore
  • CRUD operations
  • Bulk import or export data - csv, json, tsv
  • Sort and filter by row values
  • Lock, Freeze, Resize, Hide and Rename columns
  • Multiple views for the same collection

Automate with cloud functions and ready made extensions

  • Build cloud functions workflows on field level data changes
    • Use any NPM modules or APIs
  • Connect to your favourite tool with pre-built code blocks or create your own
    • SendGrid, Algolia, Twilio, Bigquery and more

Rich and flexible data fields





. . . . . . .
Terabox Video Player