You want to delete a file.
A quick search shows three direct imports. That's easy enough to see.
The harder question is what those three files are used by.
One may be a layout dependency. Another may be part of navigation. A third may feed a page that doesn't look related at first glance.
So the real question isn't "Who imports this file?"
It's "What breaks if I remove it?"
Most tools don't actually keep a dependency graph around as something you can ask questions of. They keep a list of files and a pile of text search. That's not the same thing.
A component tree is not a dependency graph
People mix these up a lot, so worth separating.
Component tree: who renders who, at runtime. Parent to child. React DevTools shows you this one.
Dependency graph: who imports who, at compile time. Directed, sometimes has cycles, crosses files and packages freely.
They're not the same shape. A provider might be a distant ancestor in the component tree, but in the dependency graph it's just one edge coming off main.tsx. You debug with the component tree. But when you're actually changing code, the thing you're navigating is the dependency graph — and that one's never had good tooling.
What a real graph actually looks like
Here's why building this is annoying, not fun-annoying, actually annoying:
Barrel chains. @/components → components/index.ts → chart/index.ts → chart-widget.tsx. Three re-exports before you hit real code. Do you keep those middle hops as nodes in the graph? Keep them and it's noisy. Collapse them and you lose the truth of what's happening. We keep the edges, mark them transitive, and let you collapse the view when you don't care.
Path aliases. @/, ~/, whatever a monorepo's workspace protocol wants to call itself this week. The resolver has to actually understand the build config to draw the right edge, and the build config itself is sometimes three files stacked on each other.
export * ambiguity. Same symbol name coming in from two barrels, and which one wins depends on order. The graph has to remember that order or it's just wrong.
Dynamic imports. React.lazy(() => import('./Page')). Static analysis can see it, but it's an edge that only activates at runtime. Should probably be a dashed line, not a solid one.
Type-only edges. import type doesn't exist at runtime. Delete that file and your app still runs, but tsc breaks. Two different kinds of edges, same graph.
Cycles. Real repos have them constantly, plenty of them harmless. The graph has to represent that, not fall over.
None of this is exotic. It's just Tuesday in a real repo.
Making the graph answer questions
The point isn't "look, we drew a graph." The point is what you can ask it:
Resolve-through — give me the file this import actually lands on, skip every barrel in between, one hop.
Inbound edges — who references me? If I change this file, what screens does it touch? This is the actual answer to that opening question.
Reachability from entry — can you get here from main.tsx at all? If not, it's dead code, or only a test imports it.
Shortest path between two nodes — how does this page even use that util? The path itself is the explanation.
Cut vertices — which files are chokepoints, where one change ripples everywhere? That's an objective basis for refactor priority, not someone's gut feeling.
None of this is a visualization trick. It's turning engineering instinct into something you can actually ask and get an answer to.
The graph is never complete, and that's fine
Real repos always have holes — a missing peer dep, an optional import, an alias nobody set up, a file that flat out doesn't exist anymore.
Two ways to handle that. Throw out the whole graph the second one edge fails to resolve, which is what most tools do. Or mark that edge unresolved and keep building the rest of the graph around it. We do the second. An incomplete graph you can still navigate beats a complete-or-nothing graph every time. (We wrote about the mechanics of that specific move — bypassing a broken import instead of stopping — in an earlier post, if you want the deep dive on that one piece.)
Graph plus symmetric editing
This is where the two pieces click together, and it's the part that's genuinely hard to copy.
Symmetric editing gives you precision inside one node — this thing on screen maps to exactly this AST node. The dependency graph gives you reach across nodes — this AST node's position in the whole repo's topology.
Put together: click a card on screen, and you don't just land on its JSX. You get its full path back to the entry point — through the route, the view, past three barrels, to the real file — plus who else depends on it. A precise map with no way to zoom out is just a precise island. A zoomed-out map with no precision is just a picture. You need both to actually navigate.
Why this matters more now that AI writes half the code, not less
The instinct is that once AI writes your code, you stop needing to understand structure. It's actually the opposite.
The mistake AI tools make most often is a graph mistake — importing something that doesn't exist, or not noticing a util is already used in five places and changing it breaks all five. The model sees a local window of tokens. It doesn't see the topology.
The part of the job that's left for a human is exactly "is this change safe on the graph" — inbound edges, reachability, cut vertices. That's precisely what a limited context window can't hold. So a navigable dependency graph isn't a nostalgia tool for people who refuse to use AI. It might be the one navigation skill a human still has to own.
And the gap in speed is not subtle. Working it out by hand means opening tabs, running greps, manually throwing out the false hits from a string search — that's minutes, and it's still sometimes wrong. A precomputed graph answers the same question, barrels and all, in milliseconds. Minutes versus milliseconds, and the minutes might be a wrong answer too.
A real walkthrough, with a sting in it
Say you're in Material Kit React and decide that logo.ts is no longer needed. Maybe the product has moved to a different branding component, or the logo is simply being removed from the application.
At first, it looks like a trivial deletion.
You check the graph and find three direct consumers: layout, nav, and not-found-view.
That's already useful. But the interesting part starts when you follow the upstream edges.
layout isn't the end of the story. It is itself used by higher-level application components. Follow that edge and you can see where the layout enters the application structure. nav leads into another chain of consumers. not-found-view comes through yet another path, eventually connecting back to the routing layer.
So instead of seeing:
logo.ts
├── layout
├── nav
└── not-found-view
you can keep walking:
<higher-level page/router>
│
┌───────┴───────┐
│ │
layout.tsx nav.tsx
(import) (import)
│ │
└──────┐ ┌──────┘
▼ ▼
logo.ts
▲
│
(import)
not-found-view.tsx
│
<higher-level page/router>
The important question is no longer:
Who imports
logo.ts?
It's:
What parts of the application are sitting above those consumers?
That distinction matters when you're deleting something.
A direct-reference search can tell you that three files need attention. It doesn't necessarily give you the context needed to understand where those three files participate in the application. You can easily remove the component, clean up the obvious imports, and still miss an affected path higher in the tree.
And that's where seemingly harmless deletions become expensive.
A shared component can sit low in the dependency graph while its consumers sit inside completely different application paths. One may affect the main layout. Another may affect navigation. Another may only appear on an error route that nobody happened to test locally.
Without the graph, the usual workflow is familiar: search references, make the change, run what you normally run, and assume the result is complete. The missing dependency may not surface until a particular route is opened, a rarely used state is rendered, or another developer hits the code path you didn't know existed.
With the graph, you can inspect those upstream paths before deleting the file.
You can see that logo.ts is not really an isolated file. It's a small node sitting underneath several higher-level parts of the application. That gives you the information needed to decide whether the deletion is actually safe, which consumers need to be changed, and how far the impact extends.
The file may take five seconds to delete.
Understanding what that deletion means is the real job.
That is the value of the graph: not merely finding references, but making the dependency footprint of a change visible before the change is made.
What this means if you're teaching React
Courses teach the component tree. The job is navigating the dependency graph.
Someone who can trace an import chain can onboard onto any codebase. Someone who only knows hooks can only onboard onto the one they were taught on.
The difference is simple: the component tree teaches you how the UI is assembled; the dependency graph teaches you how the codebase is connected.
That matters the moment something stops working.
A student sees a blank screen. Instead of guessing which component is broken, they can start from the component they were working on and follow its dependency chain. One import leads to another, then another, until the broken dependency becomes visible.
The graph turns:
“Something is broken.”
into:
“This dependency is breaking this path, which affects these components.”
That is a much more useful debugging skill.
It also changes how students learn unfamiliar codebases. They don't have to understand the entire project first. They can start from something they recognize — a page, a component, an import — and navigate outward through the graph. Each edge answers a concrete question: where does this come from, and what depends on it?
This is particularly valuable when the application itself can't help you. A broken import can prevent the UI from rendering, but it doesn't erase the source-level dependency graph. You can still trace the chain, locate the failure, and understand what is happening even when the canvas is dead.
And you can deliberately turn that into a lesson.
Rename an export in a "Shadcn Admin" template. Break an import. Watch the affected part of the graph change. Then trace the path back to the change that caused it.
Now students aren't just being shown a correct React example. They're learning how to investigate a system when their change causes something else to fail.
That's the real value.
The goal isn't to teach students another React API. It's to give them a way to enter an unfamiliar codebase, understand its structure, trace problems, and keep working when the obvious surface — the running application — stops telling them what is wrong.
And barrel traversal made visible, in one click, fixes one of the most persistent beginner misunderstandings there is: that an import path tells you where the code actually lives. It usually doesn't.
Three questions for any tool claiming to handle this
- Can it jump through a barrel in one hop and land on the real definition?
- Can it tell you who references a file, including references that came in through a re-export?
- And when an import can't resolve, does it give up on the whole graph, or keep working with everything it can still see?
The ultimate debugging experience:
- Can you hover over any file to see its import/export flow in real-time?
- Does it trace a crash back to the exact broken file in the import chain?
- Can you define custom interceptors to bypass failing modules and keep your app rendering?
Worth testing on whatever you're using now. We're not going to tell you what it'll find.
What this graph looks like on a real template
We ran this against a real, popular template, not a toy repo. Because the full dependency map is massive, click the image below to download the high-resolution version directly.
Material Kit React dependency graph
Shadcn Admin dependency graph


Top comments (0)