How to move large-scale React UI-components codebase to TypeScript
Hi, my name is Ivan Grekov and I am a frontend developer at Bumble — the parent company operating Badoo and Bumble apps
Our frontend team’s main responsibilities are to deliver new user interfaces and support existing ones across our different applications, mostly within Badoo and Bumble websites. At a certain point in time, we started working on more projects in parallel and this resulted in our code needing greater reusability/stability. In order to achieve this, we decided to rewrite our React UI-components to TypeScript.
At the start of our project, we had 630 UI-components to convert in different internal UI libraries. I am going to describe how we delivered this project without interrupting feature delivery and organised stage-by-stage onboarding to TypeScript for our UI-engineers, who were new to writing TypeScript.
Why we decided to do it
Our decision towards TypeScript was based on previous experience of our frontend department who had applied TypeScript in order to have type safety for business-logic shared across some of our projects.
We had already separated business logic from UI components, which was of great help during the code support phase for our apps. Once we started creating shareable modules with business logic, our frontend team wrote it in TypeScript.
Thus our UI-unit had a clear understanding of the benefits of the business logic and we decided to use this approach too. First of all, it helped us with consistency across our codebase — having it in TypeScript. Secondly, we also benefit from type safety for UI-components.
Our ultimate goal was to make our apps more reliable and the process of writing code more predictable. Nevertheless, the project proved complex due to another factor — not everyone in our UI-unit had previous experience with TypeScript, so we had to ensure a smooth learning curve for every member of our team. Firstly, we discussed all the benefits we would get with TypeScript:
- Reduced time spent on writing code/checking code. Once a component is written, it can be reused on different projects, which saves time.
- Higher quality production code when we have type checks during the build, rather than in runtime. Most of our React-based apps and projects had already benefited from using prop-types. But the main problem was that these type checks had not been enforced during build and bundling of apps. This meant that if someone was changing API or type for one component, there was a chance of breaking changes. Thus it was resulting in desync between component types on different layers and in turn, bugs or warnings.
What we had in the beginning
Our codebase is the basis for several separate projects. We use one code style and code-quality requirement for all of our supported web-apps. These apps’ frontend part is built in React. js.
We divided business-logic and UI-representation following the frontend architecture approach, which was already used in our department. Thus we have 2 component types:
- ui-components, which usually are written as stateless functions.
It helps us to write and refactor UI components without needing to touch business logic, as mentioned before. This provides the benefit of being able to perform experiments and maintenance without holding up feature development.
Thus we had a set of goals to achieve and we needed to organise communication between all participants in this project. This was one of my main responsibilities. I was the point of contact on all project-related matters for all the team members. I was also providing on-going progress reports to management to ensure the project remained transparent, with clear expectations and that it achieved the desired end result.
Our key objectives for this project were:
- Onboard team to work with TypeScript
- Avoid hindering feature development process during migration
I started by evaluating our input data. Since we had 5 team members and all of them had different degrees of knowledge of TypeScript and an existing work-load with various ongoing projects, it was important to start transitional work not only with code, but with the team as well.
I decided to focus on three key areas — code, process and team.
When I started planning work on the project and was thinking about how to work with the codebase, I found an application for management method — PDCA to work with the codebase and team. Let’s see how it can be applied.
Evaluation of code state
First of all, when starting a large-scale project you need to know what you are working with. With this in mind, we need to start with generic quality and quantity indexes and measurements:
In order to roughly calculate the amount of work that will be required we look at how many components we currently have. In our case we used cloc. It gave us the following picture:
Of course, this data need some interpretation: in mid-January we had 554 JSX files in one project, comprising 227 components and 227 related to components spec-files with VRT-tests. We already had 62 components transpiled in TypeScript and 62 related to tests.
This data is an excellent transparency marker as regards team progress — every week we were able to measure the progress of the project and compare it against our expectations. We set midday on Friday as the measuring point, since we usually have 2 releases a day Mondays to Thursdays but morning-only releases on Fridays.
For us, it meant that we were able to measure how many components had been transpiled and shipped to Production on that day. It gave us the opportunity to be flexible and recognise when the team was struggling to keep up or when our focus was shifted to other projects. Also, it meant that we were able to provide regular feedback and communicate our progress to the management, keeping our project expectations from this project transparent.
Coverage is key when measuring changes in the codebase. Any code migration becomes easier with implemented tests. The two most important questions to ask during the initial planning phase are: Do we have tests for the components? What percentage of our components is covered by tests? These are the most important questions during initial planning.
Visual regression tests are especially helpful in this type of project. They allow you to focus on testing functionality while spending less time on visual tests due to stable coverage.
Planning is an important part of any project. Here we divided the workload across the team and evaluated progress as we progressed. The effectiveness with which we initially planned the project would determine how quickly we would be able to spot the need for adjustments and apply them.
Graph to explore dependency
If you know the quantity of codebase you can make some assumptions about the complexity of work, but this is still only one side of the process, and actually just the tip of the iceberg. Let’s look more closely at the complexity factor.
Before starting the work with code we need to check how it is interconnected. We need to know which components are codependent and based on it — and set the order of components to work with. For these purposes, we need to create dependency graphs for our components. We used a tool called madge which builds graphs based on project structure. The key feature for us was that madge is able to build graphs based on webpack configuration. This tool also has plenty of options to tune, which helps to create a proper map.
Using the generated graph, we started with components (see right side of the graph) and specifically those that are the most independent, with no/minimal dependencies. We moved these components to Typescript first.
This approach helped us with problems related to component-type complexity and allowed us to distribute work among team members such that issues created by overlapping components or types based on several components’ interfaces, were avoided. Based on this data, updated each week, we were able to build a queue of components to transpile. The graph generated each successive Friday features fewer and fewer dependencies and components left to work with. It was also a good index of the project’s progress and helped with decision-making around pace.
Estimating component complexity
So, you have made a list of components to transpile and listed the tasks required. You have also decided the order in which they will be transpiled — using the dependency graph. But how do you now assign these tasks most efficiently across the team? For this, you need to look at the components and mark them in the task list with certain tags for developers. In our project we introduced the following convention and filtering tags, and shared it across the team:
- TS basic component is one with a simple and straightforward interface and no/minimum dependencies.
- TS component is an average component. where you can find an understandable interface and see the minimum number of dependencies.
- TS view is a complex component with a composite interface and possibly a long list of dependencies.
Based on how familiar with TypeScript each developer was, and where they were on the learning curve, they started working with basic components, processing to the more complex.
Initially we agreed that I would assign the tasks to the developers. After a while, the more familiar with proceedings the team became, the more independence it developed and team members started working with tasks by themselves.
Further questions on component complexity to bear in mind while planning this kind of work include: Can this component be auto-transpiled? Is this component stateless/stateful? Does it have enums/unions in type? Does it require change of type or writing a new type?
Creating project milestones
As I mentioned earlier, your milestones should be data driven, tailored to the size and complexity of your project. Once you have evaluated it, divide it by component number, depending on the size of your team. For me, it was easier to measure progress weekly because of our release cycles, but you could also do it fortnightly or monthly. I opted for Gantt charts to help keep track of time spent on this project. They also helped maintain a level of transparency and facilitated regular reporting.
Before starting actual coding there were several initial steps that needed to be followed:
Milestone 0 — set team knowledge base
Certain decisions helped us to save time as a team. One of them was that we would work in sync, which we did by sharing knowledge. Each member of the team either had prior knowledge of TypeScript or was on a learning curve in order to speak the same language. Sharing conventions about code-styles in one big document across the team proved highly beneficial and is recommended.
Firstly, it saves time because each member can check accumulated knowledge on specific questions instead of having to look it up on the internet.
Secondly, during the process team conventions can change over time. This is quite normal. And having open communication about it is also healthy. And having one up-to-date knowledge source is great help.
We included different themes and recommendations in our shared knowledge base. Here are just some:
- Type conflicts with CSS inlined styles if they are defined as a separate const/object,
- Use global components, which are not imported and not used in props inside components
- Use several enums in one, with set of the key for each first value in each enum,
- Use union for an array of predefined values arrays.
Milestone 1 — prepare onboarding
It is important to make sure that onboarding flow suits the team well. At the outset we provided team members with starting reading lists. People were already working on other projects so, at their own pace, once they had completed their preparation they received their first tasks, starting with Typescript components labelled ‘basic’.
During the review process we provided more data on code convention. At the review stage, developers are already aware of code conventions from the existing shared knowledge base, so the process is fast and this was the case for us as well. Moreover, during checks for major errors during compilation of TypeScript and VRT tests during the review process developers have a safe space for learning and deploying these tasks with less stress. In that case VRT helps not only to maintain high code quality on the project, but also to reduce stress involved in learning new technology, and it also saves time on testing the visual changes.
Advice: Delegate tasks, set track for progress
As mentioned before, tracking progress with tech tools and the project’s Gantt chart will help you scale a project to your team capacity. If there is potential to finish the project faster by sharing it across developers, you might need to delegate the tech part of work and ask for more people to take part in the project. When based on accurate progress data, such requests are more convincing. With this in mind, we are now set up and ready to start work. Now it is time to start actual coding.
Having mentioned some of the approaches earlier, I would now like to focus on the most important practices. At this stage of the project it is important to track essential indices regarding the progress of the whole team.
Use measurable KPIs
It might have been mentioned before and sound quite straightforward, but good communication is based on transparency. Transparency in project delivery can be achieved by use of KPIs, understood both by team members and management. For this project we used the following:
- codebase coverage on production — % of code, which is now in TypeScript, that was deployed.
- engagement of UI developers — how many team members are already familiar with technology and use it.
- pace of work — how many tasks developers delivered in this period.
Communicate within the team
Rather than fixing any specific additional weekly meetings for this project, we used our normal weekly unit retrospective meeting to discuss progress. It took just one hour to discuss each member’s weekly progress within their area of responsibility. During these meetings I would report on project progress and ask if there were any specific issues to address. Most of the time any issues were resolved before the meeting and solutions were shared in team conventions documentation. Also, once a week I would report on our progress to our department management and answer any questions they had.
During this project we often found old code patterns and corner-cases and in order to resolve the issues efficiently we shared knowledge via team conventions documentation. I am mentioning this again because it is an important part of the process — share updates across the team as soon as changes appear.
Report progress on regular basis
This is an important part of your project-manager role. You need to compare indices regularly and keep your progress data up-to-date.
I mentioned the tools madge and cloc which we used for purposes of initial evaluation of codebase. At the same time every week I used the same tool-set used in our initial evaluation to collect data which was then compared to assess our team’s progress.
Week on week, you can see how fast you and your team are moving. It is useful to pause from time to time and ask — how we can improve these numbers? Is everything fine or do we need to speed up? And if so, how? Answering these questions certainly helped me to maintain transparency during the project.
We finished this process in approximately three months. All our developers are now on board with TypeScript and are writing new components in it.
Because of how we organise task division, we were able to allocate team time between writing new code and rewriting old code. We were able to balance the task set optimally for each developer every week.
Successful team onboarding
One of our key goals was to onboard our team and share knowledge about technology. As a result of this project, skillset are now aligned within the team and all members now write in TypeScript.
Fully transparent process
Every week we synced progress within the team and with management.
The communication procedures established for data exchange within the team, described earlier, helped us a lot. Data gathering for reports didn’t take a lot of time because it was mostly an automated process.
More reliable, faster development process
Because certain errors were now caught during build, not during QA-stage or in production, it really made a difference in the development flow. Also, due to prettier/eslint in pre commit hooks, developers are now incapable of making major generic mistakes, so avoiding app function failure.
Good advice: choose the right toolset
Whilst it is up to each developer to choose the tools they consider most appropriate during a project, at the same time allow me to provide some advice here — use transpilers, codemods and snippets e. g. this VScode extension from Lyft in one click helps to rename file and move to TypeScript React function components structures and types. It supports React from 16. 3. 14 and can be tuned in order to support newer syntaxis if needed. Although at the time of publishing the article, this extension is archived, but it still works fine for our purposes.
You also can use specific codemods for transpiling. These are available now on github and npm. As long as it is helpful to transpile components and the quality of code is not compromised, it makes sense to do so and free up time for other work.
Thus, this advice might also be equally applicable for any large-scale tech migration process related to Frontend.
Recent articles related to React and TypeScript
Here is a list of articles that cover other aspects of moving React UI components to TypeScript. While I was working on this article in the first half of this year, a lot of great material on Typescript was published. I have split the article into 5 groups, depending on what stage of working with TypeScript you are at.
- https://www. ably. io/blog/TypeScript-is-making-programming-better
Articles on setting TypeScript for codebase:
- https://2ality. com/2020/04/TypeScript-workflows. html
- https://2ality. com/2020/04/webpack-TypeScript. html
- https://typeofnan. dev/setup-a-TypeScript-react-redux-project/
- https://blog. bitsrc. io/react-TypeScript-cheetsheet-2b6fa2cecfe2
- https://www. smashingmagazine. com/2020/05/TypeScript-modern-react-projects-webpack-babel/
- https://indepth. dev/setting-up-efficient-workflows-with-eslint-prettier-and-TypeScript/
Articles on moving existing react codebase to TypeScript:
- https://www. twilio. com/blog/move-to-TypeScript
- https://blog. bitsrc. io/react-js-to-TypeScript-how-to-migrate-gradually-d82026126d29
- https://www. executeprogram. com/blog/porting-to-TypeScript-solved-our-api-woes
- https://www. sitepoint. com/how-to-migrate-a-react-app-to-TypeScript/
- https://fettblog. eu/TypeScript-react/
- https://alligator. io/react/TypeScript-with-react/
- https://dropbox. tech/frontend/the-great-coffeescript-to-TypeScript-migration-of-2017
Articles on writing new react codebase in TypeScript:
- https://blog. bitsrc. io/build-a-tic-tac-toe-game-with-TypeScript-react-and-mocha-ce6f1e74c996
- https://www. freecodecamp. org/news/a-practical-guide-to-TypeScript-how-to-build-a-pokedex-app-using-html-css-and-TypeScript/
- https://levelup. gitconnected. com/create-a-react-component-library-with-TypeScript-and-storybook-ed28fc7511f2
- https://dev. to/digitalocean/how-to-build-a-customer-list-management-app-with-react-and-TypeScript-39np
- https://developer. hpe. com/blog/using-TypeScript-in-grommet-applications
Articles for deeper dive in TypeScript: