Skip to content
Chintan's Blog
Go back

react-xray: Which Pages Render Which Components — and Which Ones Are Dead?

Edit page

react-xray

Every long-lived React codebase ends up in the same place: hundreds of components, dozens of routes, and nobody quite sure which <Thing/> is still used by which page anymore. Knip will tell you about unused exports. Madge will draw you a file graph. Bundle analyzers count bytes. None of them answer the question that actually matters in a code review:

“Is this component still rendered by any page in the app?”

So I built react-xray — a static analyzer CLI that produces a per-page → component reachability matrix, two flavors of unused-component report, and a tally of how much you actually use each third-party UI library. The output is one self-contained HTML file you can drop straight into a PR.

It’s open source, MIT-licensed, and lives on npm as @chintandiwakar1/react-xray.

Table of contents

Open Table of contents

The Gap Between Existing Tools

I’ve used all of these and they’re great at what they do — but none of them answer the page-reachability question:

ToolWhat it doesGap react-xray fills
knipUnused exports at the file levelDoesn’t tell you which components are rendered by which pages
madgeFile-level dependency graphNo concept of pages, components, or external library adoption
react-scannerCounts JSX render sitesNo definitions, no pages, no reachability
Bundle analyzersBytes per chunkComponent-blind

A component can be exported, imported, and still never end up on screen — because the only file that imports it is itself unreachable from any page. That’s the case I care about, and it’s the one that’s hardest to catch by eye.

Install and Run

Zero config on Next.js projects:

# one-shot run, no install
npx @chintandiwakar1/react-xray

# or globally
npm install -g @chintandiwakar1/react-xray
react-xray

Point it at any React/Next.js project and three things happen:

  1. A terminal summary prints (tokei-style table).
  2. react-xray-report.json is written — full machine-readable scan result.
  3. react-xray-report.html is written — one self-contained file, dropable into a PR.

Sample terminal output

react-xray v0.0.2   /path/to/your-app   next-app   scanned 1781 files in 2.66s

┌─────────────────────┬───────┬────────┬─────────┬───────────┐
│ Language            │ Files │ Lines  │ Code    │ Comments  │
├─────────────────────┼───────┼────────┼─────────┼───────────┤
│ TypeScript (.tsx)   │  1387 │ 382863 │  345817 │     14978 │
│ TypeScript (.ts)    │   384 │  32449 │   29819 │       892 │
│ JavaScript          │     6 │    308 │     295 │         4 │
├─────────────────────┼───────┼────────┼─────────┼───────────┤
│ Total               │       │ 415669 │         │     15874 │
└─────────────────────┴───────┴────────┴─────────┴───────────┘

Components       1494 (used 1234 · unused 260)
Pages            252
External usage   406 packages, 6332 instances

⚠  Top unused
   UserProvider           src/contexts/UserContext.tsx           26 LOC
   InvoiceTransactions    src/components/invoice-transactions.tsx  858 LOC
   LeadContactInfoComponent  src/components/lead-contact-info.tsx  18 LOC
   … 257 more — see report.html or report.json

That “858 LOC of unused code” line is the kind of finding that pays for the tool the first time you run it.

Two Kinds of Unused

react-xray distinguishes two failure modes, because they have different fixes:

ReportWhat it catches
Strictly unusedComponent is defined but no <JSXTag/> anywhere in the project renders it
UnreachableComponent’s file isn’t reached by BFS from any page — catches orphan subtrees that import each other but no page imports them

The second case is the one tools usually miss. A component can look “used” because something imports it — but that something is itself an orphan. react-xray walks the file-level import graph from every page and flags anything BFS doesn’t visit.

Reachability from pages

Every finding ships with a reasons[] array — concrete facts like “no JSX usage in project” or “file not reached by BFS from any page”. No opaque confidence scores. You decide.

How It Works

Six stages. Each stage is a pure function emitting a plain object, so every stage is independently testable and the whole thing is roughly as fast as tsc --noEmit:

Pipeline

Why the graph is file-level, not component-level

This was the most important design call. A page transitively renders things it never directly imports — children, slot props, as-prop rendering, render-prop callbacks. Trying to build a true component-render graph in a static analyzer gives you a graph that’s wrong with confidence.

A file-level import graph is conservative in the right direction: if a file is reached, anything it exports could render. If a file is unreached, nothing in it can render. That’s the strong claim the unused report leans on.

What It Detects

The Bits That Got Hard

Two things ate disproportionate time:

1. Distinguishing components from PascalCase non-component consts. Code like const Inter = localFont(...), const QuerySchema = z.object(...), or const Body = cva(...) looks exactly like a component definition. v0.0.1 flagged hundreds of these as “unused components.” v0.0.2 ships with a denylist of factory callees that produce non-component values, plus a heuristic that components are functions whose body returns JSX or a React.createElement(...). False positives went from “annoying” to “rare.”

2. Resolving bare specifiers on projects without node_modules/. Initially, if you ran react-xray in a fresh clone, react, next/link, and friends came back as unresolved, and the external-library tally was empty. Now uninstalled bare specifiers are classified as external so library-adoption tracking works on CI runners and clean checkouts.

Caveats — Worth Being Honest About

Static analysis of a dynamic language has limits, and I’d rather you know them upfront than discover them mid-review:

When to Reach for It

What’s Next

v0 covers Next.js (App Router, Pages Router, both with or without src/). Coming next:

Try It

npx @chintandiwakar1/react-xray

That’s the whole onboarding. No config, no API key, no account.

If you find a component it falsely flags as unused, please open an issue with the snippet — false-positive reports are the most valuable kind of feedback this tool can get right now.


Edit page
Share this post on:

Next Post
remote-terminal: Access Your Dev Workspace From Anywhere