Picture this: you’re a frontend dev, six months into a project, and some designer tweaks the HTML structure. Boom—your querySelector calls shatter like cheap glass. Suddenly, hours lost chasing ghosts in selectors. That’s the quiet nightmare this approach fixes.
For everyday coders grinding on real apps, ditching querySelector isn’t some purist flex. It’s survival. It hands you DOM access that’s structural, not string-dependent—resilient to the inevitable churn of classes, IDs, or nesting shifts.
Why querySelector Feels Like a Trap
And here’s the kicker. querySelector pretends to be elegant, one-liner magic. But under the hood? It’s parsing CSS strings at runtime, every damn time. Fragile as hell if your markup evolves—which it always does.
The original post nails it:
Bad practice, because if there is a new element to be added at a later stage then things are broken.
☛ Good practice is just to stick to ‘firstElementChild’ and leave ‘lastElementChild’ free for future use.
Spot on. Selectors couple your JS too tightly to CSS. Change a class? Hunt down every reference. It’s the opposite of separation of concerns.
But. Native traversal? That’s baked into the DOM tree itself. firstElementChild. children. nextElementSibling. These don’t care about your styles—they follow the element hierarchy, which stays stable even as content dances.
The Hidden Power of DOM’s Built-Ins
Look, you’ve got an arsenal right there in vanilla JS, overlooked because querySelector stole the spotlight.
Start simple. document.activeElement grabs focus without fuss. parentElement climbs up. children.length checks siblings without arrays exploding.
In the post’s example, they map this to a single-child parent: firstElementChild === lastElementChild. Smart. But they warn against leaning on lastElementChild—future-proofing in action.
Now scale it. Their getDomObjects() function chains these bad boys:
body.firstElementChild → wrap_ctn
wrap_ctn.children[0] → top_ctn
main_elem.firstElementChild → workbench_ctn
No strings. No regex nightmares. Just tree walking, fast as native gets.
It’s OOP gold, too. They wrap it in a Map-based createObjects(), spitting out a constellation of refs. const {body_elem, wrap_ctn} = get_objects; Destructuring heaven.
From Static to Dynamic: Extending the Map
One function for existing elements. Another for the wildcards.
getDomObjectsExtended(obj_args) takes the base objects, then drills deeper: workbench_ctn.firstElementChild for wb_content. controls[1] for open_close_block.
Even toggles get nested: { tb_toggle: open_close_block.firstElementChild, mb_toggle: open_close_block.lastElementChild }
This isn’t hackery. It’s architecture. A central hub—your Map—that radiates out to every node you need. Refactor HTML? Update one traversal path, everything flows.
Pause here. My unique angle: this mirrors the pre-jQuery era, when DOM scripting meant raw traversal. jQuery abstracted it away with $(), birthing selector laziness. But as SPAs ballooned, that laziness bit back—hello, virtual DOM escapes in React. Today’s vanilla revival? It’s circling back smarter, unburdened by libs.
Does This Scale for Messy, Real-World Apps?
Short answer: yes. But let’s test it.
The post’s HTML is tidy—semantic sections, mains, asides. Controls container with dynamic sidebars. Fine for prototypes. But production? Shadow DOM? Frameworks injecting nodes?
Here’s where it shines. Since you’re not selector-hunting, dynamic inserts play nice. Add a child? children.length adapts. Sibling shifts? nextElementSibling reroutes.
Critique time. The code’s async—await FT.createObjects. Why? Probably for dynamic loads or modules. Smart, but exposes a gotcha: if DOM ain’t ready, you’re null-checking hell. Solution? Stick it post-DOMContentLoaded, or wrap in a ResizeObserver for mutations.
And for complex trees? Combine with custom elements. Your workbench-ctn becomes a web component, exposing traversal hooks internally. Hybrid power.
Why Does This Matter for OOP JavaScript?
The author works ‘Javascript OOP way.’ Traversal feeds that perfectly.
Objects from DOM. Methods on ‘em. No global soup of selectors scattered in event handlers.
Imagine: class App { constructor() { this.dom = await getDomObjects(); } toggleToolbar() { this.dom.toggles.tb_toggle.click(); } }
Encapsulation. State lives in the tree, not strings. Testable—mock the Map. Maintainable—grep ‘firstElementChild’, see the paths.
Corporate hype check: none here. This is indie dev grit, no VC gloss. But prediction? As Tree Shaking chews frameworks, vanilla DOM patterns like this will explode. Bun’s rise? Speed demons will flock to zero-overhead traversal.
Real-World Pitfalls (And Fixes)
Won’t lie. Not everything’s roses.
Single-child assumptions break if designers multi-child everything. Fix: always check children.length > 1, fallback to querySelector as last resort (gasp).
Performance? Traversal’s O(1) per hop—blazing vs selector scans. But deep trees? Cache in that Map, invalidate on mutations via MutationObserver.
Accessibility? Semantic HTML assumed. Pair with ARIA roles, and you’re golden—traversal respects outline order.
The Bigger Shift: Architectural Resilience
So. We’ve got the how: chain natives, Map-ify, OOP-ize.
Why? Frontend’s shifting. SPAs to islands (HTMX, Alpine). SSR everywhere. Selectors can’t keep up—they’re CSS baggage in a post-CSS-in-JS world.
This method? Framework-agnostic. Vanilla pure. Your app’s skeleton hardens.
Try it. Next project, kill querySelector. Watch refactors shrink from days to minutes.
🧬 Related Insights
- Read more: Ethereum’s Hidden Gem at 0x09: Verifying Zcash Proofs for 22K Gas
- Read more: Cloudflare’s Workers AI Ignites Agents with Kimi K2.5’s Massive Brainpower
Frequently Asked Questions
What are alternatives to querySelector?
Native DOM traversal: firstElementChild, lastElementChild, children, nextElementSibling, parentElement. Chain them for strong access.
Is querySelector slow or bad?
Not always slow, but brittle—ties JS to CSS strings. Breaks on markup changes, hard to refactor.
How do I start ditching querySelector today?
Map your DOM tree post-load using functions like getDomObjects(). Centralize refs, traverse structurally.