Moving the component library while the team kept shipping
115 of 120screens in the main app import the library
I extracted and reshaped the team's React component library out of the main app and into its own package, in one 1,741-file change that landed while about ten feature PRs merged around it. Three apps build on it now.
- Isherwoods
- 2025
- Private
115 of 120screens in the main app import the library
This is employer work, so it's described rather than shown. Names are generic and the diagram is redrawn. There's no code or UI from the product.
01Problem
The team's React component library was good, but it had grown into the main app it was built in. It imported app code 65 times, 17 of them into the app's API layer. It reached into the app's modal registry, an image-library hook and an admin screen's toolbar types. The generic details screen, which every record type uses, had the app's chat wired straight into it. Inside, 59 files imported across the library's own areas. Nothing else could use it without pulling in the main app.
The team didn't stop for this. Feature work kept landing on the same code while my branch was open. The product was also partway through its move to Microsoft's Fluent 2, and the theme lived in the app.
02Decisions
Find every place the library reached into the app
I could have moved the files and fixed whatever broke. In a codebase this size, that means finding the breakages one build at a time. So I listed every crossing first: the library's imports into the app, and between its own areas. That turned the job into a list I could work down, and I'd know when I was done.
Cut each link so the library doesn't know about the app
Each import got the same call. If it was library code, it moved into the library. If not, it went back out to the app. Chat is the clearest case. The details screen is generic, so it shouldn't know chat exists. I moved chat into the main app, behind a hook and an app-level details screen. The library's screen stays generic, and the app that wants chat adds it.
Land it as one change on a long-running branch
The other option was a run of smaller PRs, each leaving the app working. I went with one PR of 1,741 files, about 119k lines added and 96k removed. The app never sat half-moved, with components in two homes. The cost was keeping the branch alive while about ten feature PRs landed on main, and bringing each of those changes across. It needed two or three fixes after it merged.
Fence the boundaries with lint, not review alone
I could have relied on code review, or written custom lint rules. I configured standard ESLint rules instead, so there's nothing of ours to maintain. They stop one feature importing another, with one exception: the list may use search. A few path and naming rules sit alongside. The package is built in layers, from screens down to utils, types and config, and imports should only go down. That part is a convention, not a check, and three small exceptions have crept in.
Bring the Fluent 2 theme across with the library
The product was already partly on Fluent UI, and its Fluent 2 theme lived in the app. It came across with the library, so every app gets the same look from the package, not from a copy. My part of the Fluent 2 work began in March 2025. It reaches nearly every screen now, and the old Fluent styles left the main app in May 2026.
03Result
The main app moved onto the package in July 2025, the replication app in November 2025 and the schema editor in March 2026. 115 of the main app's 120 screens import it. It has one public entry point and five layers.
I built the package with Rollup first, and it was slow. Adding an esbuild plugin to Rollup took the JavaScript build from about 30 seconds to 2 or 3. The type files are what slow it down now, which moving to TypeScript 7 could fix in time.
04How I worked
Four developers built the library inside the main app from January 2023, and colleagues did the early Fluent UI work from 2022. During the extraction, two senior colleagues were working in the same repo. The extraction and the decisions above were mine. From July to September 2025 I made 11 of the library's 16 follow-up commits.
- React
- TypeScript
- Fluent UI
- Rollup
- esbuild
- ESLint
- Lerna
- Nx
05What I'd do differently
I'd land it in smaller PRs, each moving one area and leaving the app working. One diff that size was hard to review well, and hard to back out. I'd also build it with tsup instead of Rollup: it's fast by default, handles TypeScript well and needs less configuration. And I'd enforce the layers with stronger lint rules from day one, so a wrong import fails the build instead of relying on convention.