How I Refactored a SPA Project

Written by sun0day | Published 2022/09/19
Tech Story Tags: javascript | code-quality | web-development | webdev | devops | refactoring | programming | optimization

TLDRRefactoring is a disciplined technique for restructuring an existing body of code, altering its internal structure without changing its external behavior. The purpose of refactoring this project is to enhance code scalability and maintainability. The project is made by **react**, **webpack,** and **typescript**. The refactoring of this project was a disaster and increased the development costs and slowed down the delivery efficiency. The following actions will be performed under these e2e test case constraints.via the TL;DR App

Background

Recently I took over a SPA(Single Page Application) web project. Apps built by this project will be delivered to different enterprise users. All users’ features are mostly similar while each user has his own customized feature. In other words, the majority of codes among apps are the same while the rest exist differences. But the engineering of this project before refactoring was totally a disaster. It largely increased the development costs and slowed down the delivery efficiency. So the purpose of refactoring this project is to enhance code scalability and maintainability.

Refactoring is a disciplined technique for restructuring an existing body of code, altering its internal structure without changing its external behavior.

In this article, I’m not gonna talk about specific code refactoring details but some critical issues of this project.

Problem & Solution

1. Don’t change app behavior

Whatever you gonna do in your refactoring process, make sure it won’t change app behavior. If you are refactoring a function, use unit tests to avoid bugs; If you are refactoring a feature or even a full source code, use e2e tests to prevent bugs. Before I started to refactor this project, it took me a while to collect e2e test cases for all pages. The following actions will be performed under these e2e test case constraints.

2. The chaos of code organization

Let’s take a look at the partial code directories of this project. This project is made by react, webpack, and typescript. The /src was divided into 3 major parts:

  • /commons: common components and utils were put in this directory
  • /apps: apps pages that contain specific business logic were put in this directory
  • index_app.tsx: apps entry files which were used by webpack

/root
  |-- package.json
  |-- /config # webpack config
  |-- /src
        |-- /commons # common modules
              |-- /components # common react components
                    |-- /app1
                        |-- ComponentA.tsx
                        |-- ComponentB.tsx
                        |-- ComponentD.tsx
                    |-- /app2
                        |-- ComponentA.tsx
                        |-- ComponentE.tsx
                    |-- ComponentA.tsx
                    |-- ComponentB.tsx
                    |-- ComponentC.tsx
              |-- /utils # common util tools
                    |-- request.ts # basic request function
        |-- /apps # apps business logic
              |-- /components # apps pages
                    |-- /app1
                          |-- PageA.tsx
                          |-- PageB.tsx
                          |-- PageD.tsx
                    |-- /app2
                          |-- PageA.tsx
                          |-- PageE.tsx
                    |-- PageA.tsx
                    |-- PageB.tsx
                    |-- PageC.tsx
              |-- /utils # common util tools
                    |-- request.ts # remote server HTTP APIs
        |-- index_app1.tsx # app1 entry file
        |-- index_app2.tsx # app2 entry file

The disadvantages of this project organization are that:

  • Code files belonging to an app were scattered in too many places which made the business logic low cohesion. commons/components/app1, apps/components/app1, index_app1.tsx should be put together.
  • Code layers’ responsibilities were not clear. commons directory should only contain common code files and not contain specific app components. apps/utils/request.ts should be hoist to commons/utils.
  • Files naming was inaccurate. apps/components were used to put pages, it’s more accurate naming it apps/pages; apps/utils/request.ts was used to define remote server HTTP APIs, it’s more accurate naming it apps/utils/service.ts.

Here I’ll give a new code organization for this project. Common components, pages, and utils were put in the same layer commons while apps specific code files stay in their own directory context.

/root
  |-- package.json
  |-- /config # webpack config
  |-- /src
        |-- /commons # common modules
              |-- /components # common react components
                    |-- ComponentA.tsx
                    |-- ComponentB.tsx
                    |-- ComponentC.tsx
              |-- /pages
                    |-- PageA.tsx
                    |-- PageB.tsx
                    |-- PageC.tsx
              |-- /utils # common util tools
                    |-- request.ts # basic request function
                    |-- service.ts # remote server HTTP APIs
        |-- /apps # apps business logic
              |-- /app1
                    |-- /components # app1 components
                          |-- ComponentA.tsx
                          |-- ComponentB.tsx
                          |-- ComponentD.tsx
                    |-- /pages # app1 pages
                          |-- PageA.tsx
                          |-- PageB.tsx
                          |-- PageD.tsx
                    |-- index.tsx # app1 entry file
              |-- /app2
                    |-- /components # app2 components
                          |-- ComponentA.tsx
                          |-- ComponentE.tsx
                    |-- /pages # app2 pages
                          |-- PageA.tsx
                          |-- PageE.tsx
                    |-- index.tsx # app2 entry file

3. Low component abstraction and reuse

As you can see, components such as ComponentAComponentBPageA abstraction degree were too low. There were the commons version, app1 version, and app2 version in these components. When I dug deeper into the code, I found that these components’ internal logic was very similar and the differences among apps could be covered through elements props. So after merging different component versions into one single file, the code organization became

/root
  |-- package.json
  |-- /config # webpack config
  |-- /src
        |-- /commons # common modules
              |-- /components # common react components
                    |-- ComponentA.tsx
                    |-- ComponentB.tsx
                    |-- ComponentC.tsx
              |-- /pages
                    |-- PageA.tsx
                    |-- PageB.tsx
                    |-- PageC.tsx
              |-- /utils # common util tools
                    |-- request.ts # basic request function
                    |-- service.ts # remote server HTTP APIs
        |-- /apps # apps business logic
              |-- /app1
                    |-- /components # app1 components
                          |-- ComponentD.tsx
                    |-- /pages # app1 pages
                          |-- PageD.tsx
                    |-- index.tsx # app1 entry file
              |-- /app2
                    |-- /components # app2 components
                          |-- ComponentE.tsx
                    |-- /pages # app2 pages
                          |-- PageE.tsx
                    |-- index.tsx # app2 entry file

4. Bad version management

Different apps have different iteration cycles due to user needs. The development and delivery of each app should be independent, which means app2 code should not be affected when developing app1 features, especially when commons code is involved. In other words, app1 and app2 should depend on a different version of commons. A simple strategy is that abstract commons into an npm package and references it in each app. Thus, each app directory should become an independent npm package too. To ensure development efficiency, I choose pnpm workspaces to manage commons, app1, app2. Finally, the project code organization becomes

/root
  |-- package.json
  |-- /config # webpack config
  |-- /src
        |-- /commons # common modules
              |-- /components # common react components
                    |-- ComponentA.tsx
                    |-- ComponentB.tsx
                    |-- ComponentC.tsx
              |-- /pages
                    |-- PageA.tsx
                    |-- PageB.tsx
                    |-- PageC.tsx
              |-- /utils # common util tools
                    |-- request.ts # basic request function
                    |-- service.ts # remote server HTTP APIs
              |-- package.json
        |-- /apps # apps business logic
              |-- /app1
                    |-- /components # app1 components
                          |-- ComponentD.tsx
                    |-- /pages # app1 pages
                          |-- PageD.tsx
                    |-- index.tsx # app1 entry file
                    |-- package.json  
              |-- /app2
                    |-- /components # app2 components
                          |-- ComponentE.tsx
                    |-- /pages # app2 pages
                          |-- PageE.tsx
                    |-- index.tsx # app2 entry fil
                    |-- package.json

Conclusion

This article describes how I refactored a multi-version code coexistence project:

  • Prevent bugs through e2e tests
  • Clarify code layers and responsibility boundaries
  • Improve code abstraction and reuse
  • Use pnpm workspaces to manage multi-version apps


Written by sun0day | Full-stack developer. Contributor of vitest, histoire, umi, storybook, vueuse, vitepress. Peace & Code
Published by HackerNoon on 2022/09/19