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. is a disciplined technique for restructuring an existing body of code, altering its internal structure without changing its external behavior. Refactoring 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 to avoid bugs; If you are refactoring a feature or even a full source code, use 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. unit tests e2e tests 2. The chaos of code organization Let’s take a look at the partial code directories of this project. This project is made by , and . The was divided into 3 major parts: react webpack, typescript /src : common components and utils were put in this directory /commons : apps pages that contain specific business logic were put in this directory /apps : apps entry files which were used by webpack index_app.tsx /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. , , should be put together. commons/components/app1 apps/components/app1 index_app1.tsx Code layers’ responsibilities were not clear. directory should only contain common code files and not contain specific app components. should be hoist to . commons apps/utils/request.ts commons/utils Files naming was inaccurate. were used to put pages, it’s more accurate naming it ; was used to define remote server HTTP APIs, it’s more accurate naming it . apps/components apps/pages apps/utils/request.ts 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 while apps specific code files stay in their own directory context. commons /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 、 、 abstraction degree were too low. There were the version, version, and 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 ComponentA ComponentB PageA commons app1 app2 /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 code should not be affected when developing features, especially when code is involved. In other words, and should depend on a different version of . A simple strategy is that abstract into an package and references it in each app. Thus, each app directory should become an independent package too. To ensure development efficiency, I choose to manage , , . Finally, the project code organization becomes app2 app1 commons app1 app2 commons commons npm npm pnpm workspaces commons app1 app2 /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