We refactored 10K lines of code in our open-source React project
Tags: javascript, react, webdev, showdev
Published: 2021-02-09 02:15:46

### 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](https://rowy.io/?utm_source=Medium&utm_medium=blog&utm_content=MediumArticle&utm_campaign=We%20Refactored%2010%2C000%2B%20Lines%20of%20Code%20in%20Our%20Open-Source%20React%C2%A0Project), 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](https://docs.rowy.io/field-types/supported-fields), 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](https://docs.rowy.io/field-types/add).

## 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:

<figure>

![An old screenshot of Rowy with a UI very similar to a spreadsheet program](https://dev-to-uploads.s3.amazonaws.com/i/kgkgz3y4szlo1q1r6psq.jpg)

<figcaption>
A screenshot of an old build from February 2020
</figcaption>

</figure>

We used [React Data Grid](https://adazzle.github.io/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](https://dev-to-uploads.s3.amazonaws.com/i/54pxk5s8j04ficgwnz4l.gif)

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](https://firebase.google.com/docs/auth/admin/custom-claims).

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](https://dev-to-uploads.s3.amazonaws.com/i/gpdidsjuxj66n9w7rzy5.png)

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](https://reactjs.org/docs/hooks-intro.html#complex-components-become-hard-to-understand) 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](https://github.com/rowyio/rowy/blob/6e81191fc26714c4c212bae19cfc4c16440c322f/src/components/fields/types.ts#L9), 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](https://dev-to-uploads.s3.amazonaws.com/i/2n9v8omxdsf9om9rb6ke.png)

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](https://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](https://twitter.com/nots_dney).

{% github rowyio/rowy %}