Components & the Containment Tree
In the last phase you learned how Ext JS classes are
declared — Ext.define, extend, the config block, xtype. Now we use those
classes to build something you can actually see. And here's the one idea that, once
it lands, makes a sprawling legacy Ext JS app readable instead of terrifying.
💡 The mental model: your UI is a tree, and you only ever write the tree — never the
DOM. You don't append <div>s. You don't call appendChild. You write a nested
config object that says "a viewport containing a panel, and that panel contains a grid
and a form," and the framework walks that tree, instantiates each node, lays it out, and
renders the real HTML for you. The structure you write is the structure of the screen.
When you inherit an Ext JS codebase, you are reading someone's tree.
Here's that tree as a config, for our running users admin screen — a grid of users on the left, an edit form on the right:
Ext.;
What just happened: We described a screen, we didn't build one. The Viewport is the
root node (it fills the browser window). Its items array holds one panel; that panel's
items array holds a grid and a form. xtype names the class for each node so the
framework knows what to instantiate. Nowhere did we touch HTML — items nesting is the
nesting. That array is the only "where does this go" wiring you ever write.
The same tree, drawn out:
flowchart TD
V[Viewport] --> P[Panel: Users]
P --> G[Grid: users]
P --> F[Form: edit user]
📝 Keep that picture in your head for the whole framework. A grid is a node in this tree. A form field is a node. A button inside a toolbar inside a panel — all nodes. Phase 5's stores feed data into these nodes, but the skeleton is always this containment tree.
Component vs Container vs Panel
Three classes form a ladder, and almost every widget you'll meet sits on one of its rungs.
Ext.Componentis the base of every visible thing. It owns a lifecycle, a position in the tree, an element it renders to, show/hide, and so on. A plain component is a leaf — it does not hold children.Ext.container.Containeris a component that can hold child components, in itsitemsarray. It adds the machinery to manage children and arrange them with a layout (Phase 4). Anything withitemsis a container.Ext.panel.Panelis the container you'll see most often: a container plus a header/title, a border, and tools (the little buttons in the header), docked toolbars, collapsibility, and so on. Grids, forms, windows, and tab panels all extend Panel — which is why they all have titles and borders "for free."
// Leaf component — no items, just renders something:
// Container — holds children, arranges them, but no chrome:
// Panel — a container WITH a title, border, and header tools:
What just happened: Same items mechanism each step up the ladder; the difference is
purely what each class adds. The bare component has no items because it can't hold any.
The container holds children but draws no header — useful for invisible grouping. The
panel is the same thing dressed up with the chrome users expect. When you see a class in
legacy code, ask "is it a Component (leaf), a Container (holds items), or a Panel
(container with chrome)?" — that one question tells you most of what it does.
The lifecycle, and why "hidden" is not "destroyed"
A component isn't just a config blob — it goes through a fixed sequence of life stages, and knowing them is how you debug "my setup code ran too early" and "why is the app leaking memory."
The Classic toolkit lifecycle, in order:
constructor → initComponent → render → afterRender → ... (lives, reacts to
events) ... → destroy
The one you override constantly is initComponent — the classic, canonical place to set
up config and build items before the component renders.
Ext.;
What just happened: We subclassed a grid and used initComponent to assemble its config
just before render — handy when a value has to be computed (a default title, columns built
from a variable) rather than written as a static literal. The critical line is
this.callParent(arguments): it runs the parent's initComponent, which is what actually
wires up the component. Forget it and your component half-initializes and fails in
confusing ways. (The Modern toolkit leans on the config block and an initialize method
instead, but the override-and-callParent discipline is the same idea.)
⚠️ Now the part that bites people: hiding a component does not destroy it. cmp.hide()
just sets it invisible — the instance, its DOM, its event listeners, and any store
bindings all still exist, consuming memory. If your code creates components over and over
(open a window, close it, open it again...) and only hides them, you have a memory
leak and stale listeners firing on ghosts. To truly free a component you must
destroy it.
win.; // invisible, but fully alive — listeners still attached, DOM still there
win.; // tears down DOM + listeners + child components; the instance is gone
What just happened: hide() is for "I'll show this again in a second." destroy() is for
"I'm done with this forever." A container's destroy() cascades — destroying a panel
destroys its grid and form too. The rule of thumb for inherited code: if you see things
created repeatedly but never destroyed, suspect a leak.
Adding and removing children at runtime
The tree isn't frozen at startup. Containers can grow and shrink while the app runs — this is how a "+ Add User" button makes a new form appear, or how closing a tab removes a panel.
var panel = Ext.; // (shown for illustration — see the warning below)
panel.; // append a child to items
panel.; // remove one child (destroys it by default)
panel.; // clear every child
What just happened: add takes a config object (or a real component) and slots it into
the container's items, then re-runs the layout so it appears. remove pulls one child out
— and by default destroys it, which is usually what you want (no leak). removeAll
empties the container. Each call re-lays-out the container, so children rearrange
automatically. Notice we had to find panel first — which is the next, and most
important, skill.
Finding components — done right
This is the single most useful skill for navigating an inherited Ext JS codebase. You'll constantly need to grab a component to read its value, refresh its store, or react to a click. There's a wrong way that's all over old code, and a right way.
⚠️ The trap: Ext.getCmp and global ids
// DON'T build code around this:
var grid = Ext.; // requires a hand-assigned id: 'usersGrid'
What just happened: Ext.getCmp(id) looks up a component by a manual id: you set in its
config. It works — but id must be globally unique across the entire app, forever. The
moment two instances of the same view exist (two tabs, a reused window), their ids collide
and the lookup returns the wrong one or breaks outright. This is a well-known anti-pattern.
You'll see it in legacy code; recognize it, and don't add more of it.
The safe local id: itemId
If you need a stable handle inside one container, use itemId — it's scoped to that
container, so it can't collide globally — and reach it with down('#...'):
// later, from the panel:
var field = panel.; // '#' targets an itemId, scoped to this panel
What just happened: itemId is the leak-free cousin of id. Two copies of this panel can
each have their own emailField without conflict, because the lookup is relative to the
container, not the whole page.
The right way: reference + lookupReference
In a modern MVVM app (Phase 7), you tag a child with reference and look it up from the
view's ViewController with lookupReference (or view.lookup in newer versions):
// In the view:
// In the ViewController:
onSaveClick:
What just happened: reference names a child within its view, and the ViewController
resolves it locally. No global namespace, no collisions, and the wiring lives right next to
the logic that uses it. This is the pattern modern Ext JS code is built around — when you
see reference in a view and lookup/lookupReference in a controller, they're two ends
of the same string.
Walking the tree: up() and down()
Often you already have one component (the button that was clicked) and need its neighbor. Walk the tree relative to where you are:
cmp.up('selector')— the nearest ancestor that matches.cmp.down('selector')— the first descendant that matches.
onDeleteClick:
What just happened: up('grid') climbs from the button toward the root until it hits a
grid — no id needed, just "the grid I live inside." down('textfield[name=email]')
descends to the first text field whose name is email. This is how event handlers find
their context: start from the thing you were handed, navigate by relationship.
Querying anywhere: Ext.ComponentQuery
The selectors above ('grid', '#emailField', 'textfield[name=email]') are
component queries — CSS-like selectors, but matching over xtypes and component
attributes instead of HTML tags and classes. up/down run them relative to a
component; Ext.ComponentQuery.query runs one globally:
Ext..; // every grid in the app
Ext..; // grids that are direct children of a panel
Ext..; // all email fields anywhere
What just happened: Same selector grammar as up/down, returning an array of every
match across the whole component tree. xtype is the "tag", [attr=value] filters by config,
> means direct child — read it like CSS for components. In a strange codebase this is your
flashlight: query for the component you can see on screen, inspect what comes back, and you've
found where it lives in the tree.
💡 The honest hierarchy of "how do I get a component," best first: reference +
lookupReference (or a relative up/down) for everyday view logic; itemId +
down('#...') when you need a stable local handle; Ext.ComponentQuery for ad-hoc
spelunking and debugging; and Ext.getCmp only when you're reading old code that already
uses it — never as the way you write new code.
Recap
- Your UI is a tree of components, and
itemsis the only nesting you write — the framework instantiates the tree and renders the real DOM. Reading an Ext JS app means reading its containment tree. - Component → Container → Panel is a ladder: every visible thing is a
Component; aContaineradds childitemsand a layout; aPaneladds a header/title/border. Grids, forms, and windows extend Panel. - The Classic lifecycle is
constructor→initComponent→render→afterRender→destroy; overrideinitComponentto set up config and always callcallParent. hide()≠destroy()— hidden components keep their DOM, listeners, and bindings alive. Destroy what you're done with, or leak memory.- Containers change at runtime with
add,remove, andremoveAll. - Find components the right way: prefer
reference+lookupReferenceand relativeup()/down(); useitemIdfor safe local handles andExt.ComponentQueryfor exploration. TreatExt.getCmpand globalidas a legacy anti-pattern.
Quick check
Lock in the one idea that matters most — how the tree fits together and how to navigate it:
[
{
"q": "What is the difference between Ext.container.Container and Ext.panel.Panel?",
"choices": [
"Container renders HTML; Panel does not",
"A Container holds child components via items; a Panel is a Container that also adds a header/title and border",
"Panel is the base class that Container extends",
"They are aliases for the same class"
],
"answer": 1,
"explain": "Container adds the items machinery to Component; Panel is a Container with chrome (header, title, border). Grids and forms extend Panel."
},
{
"q": "You call cmp.hide() on a window and reopen a fresh one each time the user clicks a button, never destroying the old ones. What happens?",
"choices": [
"Nothing — hide() fully frees the component",
"Ext JS automatically garbage-collects hidden components",
"The old instances stay alive with their DOM and listeners, leaking memory",
"The new window reuses the hidden one automatically"
],
"answer": 2,
"explain": "hide() only makes a component invisible. Its DOM, listeners, and bindings stay alive until you destroy() it — so repeatedly hiding instead of destroying leaks."
},
{
"q": "Which is the recommended way to get a child component in modern Ext JS, and which is the anti-pattern to avoid?",
"choices": [
"Recommended: Ext.getCmp('id'); avoid: reference + lookupReference",
"Recommended: reference + lookupReference (or up()/down()); avoid: Ext.getCmp with a global id",
"Recommended: document.querySelector; avoid: Ext.ComponentQuery",
"Both Ext.getCmp and reference are equally fine"
],
"answer": 1,
"explain": "Global ids must be unique forever and collide on reuse. Prefer reference + lookupReference (or relative up()/down()); Ext.getCmp is a legacy anti-pattern."
}
]
← Phase 2: The Class System · Guide overview · Phase 4: Layouts: How Things Get Positioned →
Check your understanding
1. What is the difference between Ext.container.Container and Ext.panel.Panel?
2. You call cmp.hide() on a window and reopen a fresh one each time the user clicks a button, never destroying the old ones. What happens?
3. Which is the recommended way to get a child component in modern Ext JS, and which is the anti-pattern to avoid?