We started working on our React application a few years ago. We started with Flux, but shifted to Redux when it became available. Over time, requirements evolved and the application grew. We also moved to newer frameworks for forms, modals and charting to name just a few. In fact, a significant part of our code now uses GraphQL so state management with Redux is reduced. We were learning as we went along and the implementation of new features often took priority over cleaning up working code.
As a result, the time was ripe for an exercise in decluttering the application. At the start of this exercise, the application had more than 500 components and hundreds of reducers and actions. So, this wasn’t a toy application.
We use ESLint so individual files were relatively clean. But ESLint will not help if a module is unused or an exported function is never called. The challenge was to remove unused code and to reorganize existing code so that the code was smaller, easier to understand, and more maintainable.
To get started, we used the utility lxjs and a commercial tool Lattix Architect to create a map of the dependencies of our application. For any file, it allowed us to see what it imports and who imports it. It was also easy to follow dependency chains. The underlying data comes from madge that can be freely downloaded.
Armed with this data, we proceeded to refactor the application.
We quickly identified files that were no longer in use. Of course, it wasn’t enough to remove just the unused files, we also had to follow it up for code that was used only by these files.
Components were the largest part of our client code. Roughly 70% of our client code was components. This is where the most work went into. We ended up dividing the components into two parts: views and components. Views were the top level components that often corresponded to pages while the lower level components were the shared components used by views. Components that were specific to a view were normally kept within that view, but we weren’t rigid about it. We knew we had reasonably clean separation when there were no cross dependencies within views and no backward dependencies from components to views.
As our application evolved, many actions were no longer in use. Since actions are used by components and reducers, looking for unused actions is not straightforward. We had to identify all actions that weren’t used by any component. It helped that we had strong naming conventions for our actions and reducers. Once unused actions were removed it was easy to identify and remove all reducers that referenced those unused actions.
Selectors were a source of unwanted couplings. While most of our selectors were in reducers, there were a few that were in components. We simply co-located the selectors with the reducers. This also helps encapsulate the state within the reducers and hides the details from the component. Sometimes selectors use other selectors. This creates a coupling between reducers. We co-located selectors with reducers to avoid creating a cyclic coupling between reducers.
Ad-hoc helpers were another source of unwanted coupling. There were helpers that were created to support one component initially, which were then used by other components. For instance, any component that dealt with time/date selection had to deal with time zone adjustment. Initially, many of these helper functions were part of the time zone selection component. These functions were moved to a separate helpers directory for use by all components.
We had more than 50 css files. Most were located in a separate styles directory but some were colocated with components. We got rid of unused css files easily. We didn’t attempt to rationalize the multiplicity of styles used by different components because it wasn’t perceived to be an issue.
It’s easier than it looks. This was a surprise. Cleanup is perceived as hard and thankless that doesn’t add to the bottom line. However, it is technical debt which takes an incremental bite every time you make a change. Initial estimates were that this exercise could take months. In fact, the bulk of this exercise was completed in two weeks.
Know your code organization. One of the valuable by-products of this exercise is that it aligns the current team. You can often organize the code by feature or by type. We used a combination of these two approaches at different levels. However, we always mediated both approaches by dependencies to ensure that we weren’t introducing cyclic dependencies.
Reducing the bundle size isn’t the goal. This is not an exercise to reduce the bundle size, even though it did help us get rid of a few libraries. The multiplicity of libraries is its own form of clutter. For instance, our application uses both Redux-form and Formik. To get rid of one or the other would help reduce the bundle size and reduce library clutter but we kept it outside the scope of this effort.
Often code starts out as well organized but erodes over time. It is easy to blame deadlines and business needs but they do not fully explain this common phenomenon. Other reasons include:
Programming culture doesn’t emphasize cleaning up. We solve business problems by using the right combination of technologies. We write tests. We then move to the next issue on our sprint. Cleaning up isn’t measured or valued in our development processes.
For current programmers, it is not a priority. The problems arise as newer people come in or as new functionality is added that others aren’t familiar with. For current programmers, the problem becomes apparent only when the application grows and they no longer understand parts of the code base developed by others.
Programmers minimize changes to code that they didn’t write. This is true even for expert programmers. Moving a helper function from one file to another requires changing unfamiliar files. Removing a piece of code requires understanding why it was created in the first case and ensuring that it really is unused. All this distracts from the task at hand and comes in the way of completing the sprint.
Without an organizing principle for the code and without a culture of continuous cleanup, code rot is inevitable.
At the end of this exercise, we ended up reducing the size of our code by about 25%. There were half as many actions and 40% fewer reducers. The number of components also came down by about 10%.
Years of accumulated clutter was gone.