Caroline Gallagher | July 19, 2017 | tag:
BitSight recently completed a reorganization of a large part of our Single Page Application (SPA) code. Our goal was to make our codebase more scalable and developer-friendly by adding a few simple rules for where different parts of the application should live. In this article, I’ll describe what we left the same, what we changed, and how we did this while continuing to ship features on time.
Starting at the fundamentals
We created our SPA using Facebook’s excellent create react app project. Some of that initial application setup code went into our base app folder. As our application grew in complexity we added Redux to manage the state and react-router for navigation, so our app folder also contains the base reducer, action, and, router. We avoided modifying this folder as much as possible during the reorganization.
During this process, we were not changing how our pages looked or behaved, just the code structure behind the scenes. Our layouts folder contains the main logic for displaying content in our pages, so it was in good shape pre-refactor and needed few changes. The core layout, which is the scaffolding (header, footer, etc) used to organize all of our SPA pages, lives there along with top-level structure for other pages.
By isolating our core application initialization and display logic from the rest of our application, we were able to change the rest of the code structure with less risk. During this process, the foundations of the application did not change, which reduced our risk of serious regressions.
If everything is a module...
Almost everything else related to the SPA initially lived in the modules folder. This included react components, redux actions, reducers and sagas, routing logic, SCSS, and tests. We did subdivide the modules into folders by function, so related components lived together in folders, reducers were with their related actions and sagas, and so on.
BitSight has been migrating our existing frontend application to a SPA architecture on a page-by-page basis. As we added pages to the SPA, we discovered that we were creating a lot of new folders for our new features. Some folders contained only code to manage state, while others had react components. A few had both state and components. Eventually we had components which were referencing actions from another folder, as an early signal that our abstractions were breaking down.
Our wake up call that we needed to fix things was when we wound up with three folders named company, companies, and companyInfoBar respectively. The company folder contained React components to display company data, the companies folder had redux actions and reducers to retrieve company data from our API, and the companyInfoBar component had more React components. As you can imagine, keeping them straight was a little tricky.
We needed an intuitive, simple set of rules to separate our code into sections. The transition to this new setup also needed to happen without negatively impacting our release cycle....then nothing is a module.
...then nothing is a module
Before our reorganization, we researched how other organizations had addressed this issue; herearethree links that we found helpful. Our main takeaway was that we needed to further separate out our SPA code based on function. By keeping everything in the modules folder, we had three types of code all living in the same place: React components to show data, state management code to retrieve and manage the data, and routing logic to navigate the application. Once we realized that we needed new folders for routing, state, and components, we dove into the nitty-gritty of where these folders would live and what they would be named.
The new routes folder was the simplest of the three. Routes had previously been stored in folders with their related components, but routing and display logic are different enough that keeping them together was not necessary. The SPA was already structured so that each top-level route had its own routing file. The main app router just imported all of those routes to connect them.
By moving all of the routes into a new folder, a developer who needed to update a route, or add new routes, would immediately know where to look in the codebase. We moved this routing folder into the app directory, since routing is fundamental to the functioning of the application. If the user cannot access all parts of the website, then things are quite broken.
The remaining code in the modules folder was split into two folders, which we named state and components. These were both new top-level folders, and replaced the modules folder entirely.
The state folder contains a subfolder for each grouping of logically similar API endpoints, with the actions, reducers and sagas for that endpoint. It also contains related state actions for that data. To keep things simple, the name of each folder matches the name for that part of the data in the state tree.
Meanwhile, thecomponents folder contains all of the react components to display the data. Those folders are organized around SPA “pages” or features, and they can be nested to keep related components in one place.
The rules for adding new components and state are now straightforward. If a developer needs to add a new endpoint, then they know to create a new state subfolder and to name it based on the API endpoint name. If they’re adding a new component, then they can either add it to an existing folder, if it is extending something, or create a new components folder. If they’re adding both, then they just do both.
Code refactoring visualization
How we migrated our code
Before we migrated any code, we created a document showing the new file structure and held a frontend team meeting to critique it. Once the team agreed on the plan, we created a story with tickets for moving each subfolder in the modules folder. We divided the tickets up between developers based on difficulty and the developer’s familiarity with the various modules; people tried to pick up the modules that they had written to make the process quicker and more painless.
One person did the initial work to create the new routes, state and components folders. After that, we migrated the remaining folders in separate pull requests and deployed the code multiple times as pull requests were merged in.
To close the loop on our example from earlier (with the company, companies, and companyInfoBar subfolders in the modules folder), we nested the companyInfoBar folder underneath the company folder in the components folder. The companies folder moved to the state folder. One developer migrated both the companyInfoBar and company folders, while another moved companies.
It has been about two months since we finished the refactoring, and this approach has scaled well so far. We have onboarded a few new frontend developers and they picked up the organization logic quickly. When we’re adding new state or components, there is less uncertainty and discussion about where to put our code. And we still shipped all of our features on time.