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:
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.
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.
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.
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.
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
- 30+…
Top comments (2)
Datagrids libraries are always a wrong decision.
Unless the scope is very restricted, they are always thousands of LOC to display just a table, they are very inefficient. Adding a simple feature requires twice (or more) the code you would have needed with a vanilla solution. Every new feature breaks an existing one.
Using datagrids is always because a yes man accepted the customer request to be able to sort / filter / perform "statistics" on everything, which is also a bad practice. Usually, data sources tend to be super messy (especially with computed fields), the data is not reliable over time ...
Stay away from those
It was pleasing and really helpful to read about your experience. I'm currently working in a company on a project with the same structures and goals. Thank you.👌