Skip to main content

Understanding Your UI as a Tree

React models the UI as a tree — and it's actually two different trees depending on the question I'm asking: one about which components render inside which, and one about which files import which. They look similar but answer different problems.

The render tree — who renders whom

Each node is a component (not an HTML tag), the root is whatever React renders first, and every edge is a parent rendering a child.

render-tree-shape.jsx
App
├─ FancyText
└─ InspirationGenerator
├─ FancyText
└─ Copyright

The thing that surprised me: this tree isn't fixed. Conditional rendering reshapes it every render. If a component returns <FancyText> in one state and <Color> in another, the tree literally has a different child depending on props/state at that moment. So "the render tree" is really a snapshot of one render, not a permanent structure.

What it's good for is seeing performance and data flow at a glance:

  • Top-level components (near the root) sit above everything else, so re-rendering them re-renders large chunks below — that's where most cost and complexity lives.
  • Leaf components (no children) sit at the bottom and tend to re-render the most often.

Knowing which is which is what makes render-performance debugging make sense later.

The module dependency tree — who imports whom

Same tree shape, completely different nodes. Here each node is a module (a file), and each edge is an import. The root is the entry file.

dependency-tree-shape.jsx
App.js
├─ FancyText.js
├─ InspirationGenerator.js
│ └─ inspirations.js // a data file — not a component
└─ Copyright.js

Two differences from the render tree worth holding onto:

  • Non-component files show up here. Something like inspirations.js (just data) is a node in the dependency tree but never appears in the render tree — it renders nothing.
  • The parent can differ. Copyright.js might be imported by App.js (so it hangs off App in the dependency tree), yet render as a child of InspirationGenerator because it was passed down as children. Imports and rendering aren't the same relationship.

Why two trees

Because they answer different questions. The render tree explains what's on screen and why it re-renders — runtime, component-shaped. The dependency tree explains what ends up in the bundle — build-time, file-shaped. Bundlers walk the dependency tree to decide what to ship, and a bloated tree means a bigger bundle and a slower first paint. Different tree, different problem.