Alexander Garcia
Rsdoctor flagged three duplicate Radix UI packages on its first run. Here's why pnpm installs duplicates, why they break React context, and how to fix them.
Read time is about 13 minutes
Alexander Garcia is an effective JavaScript Engineer who crafts stunning web experiences.
Alexander Garcia is a meticulous Web Architect who creates scalable, maintainable web solutions.
Alexander Garcia is a passionate Software Consultant who develops extendable, fault-tolerant code.
Alexander Garcia is a detail-oriented Web Developer who builds user-friendly websites.
Alexander Garcia is a passionate Lead Software Engineer who builds user-friendly experiences.
Alexander Garcia is a trailblazing UI Engineer who develops pixel-perfect code and design.
I added Rsdoctor to my Rsbuild project to get a build-level view of my Rspack output. Within the first run it flagged three duplicate packages I hadn't intentionally installed: @radix-ui/react-slot, @radix-ui/react-context, and @radix-ui/react-primitive. Each one had two copies landing in the bundle from minor version mismatches in transitive dependencies. This post walks through why it happened, why it matters for React component libraries specifically, and how pnpm.overrides collapses them into one.
I was casually browsing the Rsbuild documentation site for performance strategies that I could then apply to my site. I had already looked into and implemented code splitting, lazy loading, image optimization and I wanted a more complete picture of what was actually happening inside my build output. The built-in bundle analyzer Rsbuild ships with shows you the size and composition of your output chunks, but it doesn't tell you much about what caused them to be that way.
Rsdoctor operates at a different level. It's a build analyzer built specifically for the Rspack ecosystem, and it captures data during the build itself which can be extremely helpful. It can give you insights into loader execution times, module resolution decisions, chunk formation, and dependency analysis if you truly need fine-grained introspection. I expected to use it mostly for the loader timing data however I didn't expect the first thing it showed me to be three packages with duplicate versions.
The Rsdoctor report has four panels: Bundle Size, Loader Analysis, Module Graph, and Alerts. The Alerts panel is where duplicate packages show up. Each alert lists the package name, the two (or more) versions that were resolved, and which modules in the graph triggered each resolution.
Setting it up in an Rsbuild project is pretty simple because its only two steps.
Install the plugin:
# pnpm pnpm add @rsdoctor/rspack-plugin -D # yarn yarn add @rsdoctor/rspack-plugin -D # npm npm install @rsdoctor/rspack-plugin --save-dev
Add build scripts that pass the RSDOCTOR environment variable. Rsbuild has first-class support for this, so you don't need to wire the plugin manually into your config:
{ "scripts": { "validate:dev": "RSDOCTOR=true rsbuild dev", "validate:build": "RSDOCTOR=true rsbuild build" } }
Run pnpm validate:build and Rsdoctor generates the report and opens it in your browser automatically when the build finishes.
Note: Windows does not support the above syntax so you might have to use the
cross-envpackage to set your environment variables across the different systems using this setup:
{ // Windows "scripts": { "validate:dev": "RSDOCTOR=true rsbuild dev", "validate:build": "RSDOCTOR=true rsbuild build" }, "devDependencies": { "cross-env": "^7.0.0" } }
This one confused me initially because I hadn't explicitly installed any of the flagged packages. They are all transitive dependencies which I'll be honest I had to look up. Essentially they are internal utilities that every Radix UI component depends on. The issue is that different Radix UI packages in my project were pinned at slightly different release versions, and each one declared a peer dependency range that resolved to a different patch:
@radix-ui/react-slot: one component pulled in 1.2.3, another resolved 1.2.4@radix-ui/react-context: split between 1.1.2 and 1.1.3@radix-ui/react-primitive: split between 2.1.3 and 2.1.4pnpm is doing exactly what it's designed to do here which is keeping your project's dependency trees isolated and won't silently coerce one package's dependencies to satisfy another. When two packages in your dependency graph ask for non-identical version ranges and both can be satisfied, pnpm installs both. The result in pnpm-lock.yaml is two separate resolution entries for the same package name:
# Before: two entries in the lockfile '@radix-ui/react-slot@1.2.3': '@radix-ui/react-slot@1.2.4':
Both end up in the bundle.
For pure utility packages the cost is mostly bundle size so you're adding more bytes. The bytes are small for these three packages individually, but the pattern compounds which could be detrimental in large codebases. The more serious risk is runtime behavior, and Radix UI is a good example of where it can break silently.
Radix UI uses React context internally to wire compound components together. The link between a Tooltip.Root and a Tooltip.Content, or a DropdownMenu.Root and its DropdownMenu.Item children, runs through React context. React context is a singleton: the provider and the consumer need to reference the exact same context object instance to communicate. If your context provider comes from react-context@1.1.2 and the consumer resolves to react-context@1.1.3, you have two different module instances. The context connection never forms. The component fails silently and might lead you to bang your head against the wall.
This is a class of bug that's extremely hard to trace. You see a Radix component not responding to state changes, you assume it's a props or event issue, you spend time in the React DevTools before eventually noticing two copies of the same internal module in your bundle. Luckily my curiosity with Rsdoctor caught this before I got anywhere near a complicated debugging session.
pnpm supports a pnpm.overrides field in package.json that forces all resolutions for a given package to a specific version, regardless of what individual packages in the dependency graph declare. Since all three mismatches were patch-level (aka no breaking API differences between 1.2.3 and 1.2.4) the pinning to the higher version was the safest.
Add the following to package.json:
"pnpm": { "overrides": { "@radix-ui/react-slot": "1.2.4", "@radix-ui/react-context": "1.1.3", "@radix-ui/react-primitive": "2.1.4" } }
Then run pnpm install. pnpm reads the overrides, regenerates the lockfile, and collapses the two resolution entries into one for each package. After reinstall it's simply:
If you're not on pnpm, the same capability exists under slightly different names. Yarn uses a resolutions field in package.json, while npm 8.3+ supports an overrides field that works identically to pnpm's. The concept is the same regardless of which package manager you're using you basically force all resolved instances of a package to a single version and only the field name differs.
# After: one entry per package '@radix-ui/react-slot@1.2.4':
Two approaches. The fast one is grepping the lockfile directly after running pnpm install:
grep "react-slot" pnpm-lock.yaml grep "react-context" pnpm-lock.yaml grep "react-primitive" pnpm-lock.yaml
Before the fix, each grep returns two version-keyed entries. After, each returns one. The duplicate is gone at the resolution level before you even run a build.
The more complete approach is running pnpm validate:build again and checking the Rsdoctor Alerts panel. After applying the overrides and reinstalling, all three duplicate package alerts were gone. The bundle composition showed a single resolved version for each.
The duplicate detection was the most immediately actionable finding, but the rest of the report is useful in different ways.
If you've been using BUNDLE_ANALYZE=true as your primary window into build output, Rsdoctor adds meaningful diagnostic depth on top of it.
Rsbuild's built-in bundle analyzer tells you what shipped. Rsdoctor tells you why.
A few things I'd carry forward from this
I had never thought to grep `pnpm-lock.yaml` for duplicate version entries, but it's a thirty-second check that surfaces exactly what Rsdoctor caught. If you suspect duplicates before investing in a build analysis tool, start there
pnpm doesn't warn you about this. Your build doesn't warn you. The component just stops working, and the path from symptom to root cause is not obvious. Tools like Rsdoctor make the invisible visible.
All three packages were mismatched by a single patch version. The `pnpm.overrides` fix took two minutes and had no behavioral impact — just one fewer copy of each package in the bundle. There's no reason to leave this kind of duplication in place once you know about it.