Warming up the neural circuits...
By the end of this chapter you will:
querySelector and getElementByIdclassList The DOM (Document Object Model) is a tree-like representation of your HTML in memory. Every tag becomes an object with properties and methods. can query this tree, change its properties, and listen for user actions (events).
<body>
<nav id="menu">
<ul>
<li class="item">Home</li> ← Each tag is a "node" (object)
<li class="item">About</li>
</ul>
</nav>
</body>In the modern era of React and Vue, we rarely touch the DOM directly — frameworks do it for us. But understanding the raw APIs is essential for:
1. Node vs. Element — A "node" is anything in the DOM tree (elements, text, comments). An "element" is specifically an HTML tag node (<div>, <p>, etc.). Most methods target elements.
2. Reflow — When you change the layout (width, height, font-size), the browser must recalculate the geometry of the entire page. Very expensive. Avoid doing this inside loops.
3. Repaint — When you change visual properties without affecting layout (color, visibility). Cheaper than reflow but still costs time.
4. Event Bubbling — When you click an element, the click event fires on that element, then on its parent, then its grandparent — all the way up to document. This is called "bubbling."
5. Event Delegation — Instead of attaching a listener to every child element, you attach one listener to a parent and detect which child was the target. Relies on bubbling.
React and Vue don't update the real browser DOM every time a variable changes. They first compute the "diff" in memory (), then batch all the real DOM changes into one operation. This minimizes expensive reflows and is why frameworks are often faster than hand-coded DOM manipulation.
document.getElementById(id) — Fastest single-element lookupReturns the element with the matching id . Fastest possible lookup — the browser has a special hash map indexed by ID.
const header = document.getElementById("main-header");
// Returns the element, or null if not found| Returns |
|---|
| Method | Selector | Result Type | Updates? | Speed |
|---|---|---|---|---|
getElementById | #id only | Element | N/A | 🚀 Ultra Fast |
getElementsByClassName | .class | Live Collection | ✅ Auto | ⚡ Fast |
querySelector | Any CSS | Element | N/A | 🟢 Moderate |
querySelectorAll | Any CSS | Static NodeList | ❌ No | 🟡 Slower |
| Mistake | What goes wrong | The fix |
|---|---|---|
el.innerHTML = userContent | XSS vulnerability — script injection | Use el.textContent for user data |
| Looping over live HTMLCollection while modifying | Skips elements as collection shrinks | Convert to array first: [...collection] |
| Not removing event listeners on unmount | Memory leak — listeners pile up | Store and call removeEventListener |
el.style.fontSize = 12 (no unit) | Style is silently ignored | Use "12px" as a string |
| Reading layout properties in a loop | Forces expensive reflow on every read | Read all values first, then write all values |
e.stopPropagation everywhere | Breaks other delegation listeners | Only stop propagation when truly necessary |
<div id="output"> without any risk of HTML injection.userInput = "<b>Hacker</b>"<b>Hacker</b> is shown as text, not rendered as bold.dark-mode class on <body> every time a button is clicked. If dark mode is active, add the class; if not, remove it.document.body.classList alternates between having/not having dark-modedocument.body. When any button with data-action="alert" is clicked, show an alert with the button's data-message attribute value.<button data-action="alert" data-message="Hello!">Click me</button>alert("Hello!") fires when button is clickedshowToast(message, durationMs) function that creates a <div class="toast"> with the message, appends it to the body, and removes it after durationMs milliseconds.showToast("Saved!", 3000)Counter class that, given a container element, builds a + button, - button, and a display. Clicking + increments the count, - decrements it (min 0). Use event delegation on the container.new Counter(document.querySelector("#app"))What's the safest way to insert user-provided text into an element?
| Use when |
|---|
Element or null | You have an ID and need maximum speed |
document.querySelector(selector) — Flexible single-element lookupReturns the first element matching any valid CSS selector. Slower than getElementById but vastly more flexible.
document.querySelector("#main-header"); // By ID
document.querySelector(".btn.primary"); // By class combo
document.querySelector("[data-user-id]"); // By attribute
document.querySelector("nav > ul > li:first-child"); // Complex CSS selector| Returns | Use when |
|---|---|
First matching Element or null | You need a flexible CSS selector |
document.querySelectorAll(selector) — All matching elementsReturns a static NodeList of all matching elements. If new elements are added to the page, this list does NOT update.
const buttons = document.querySelectorAll(".btn");
// ❌ buttons.map is not a function — NodeList is not an Array
// ✅ Convert first
const ids = [...buttons].map(btn => btn.id);
const actives = Array.from(buttons).filter(btn => btn.classList.contains("active"));NodeList vs. HTMLCollection:
| Feature | querySelectorAll result | getElementsByClassName result |
|---|---|---|
| Type | Static NodeList | Live HTMLCollection |
| Auto-updates? | ❌ No | ✅ Yes — dangerous in loops! |
.forEach() | ✅ Modern browsers | ❌ No |
| Convert to array | [...list] | [...collection] |
If you loop over getElementsByClassName results and remove elements during the loop, the collection shrinks and you'll skip elements. Always convert to an array first: [...document.getElementsByClassName("item")].
| Method | Selector type | Speed | Returns |
|---|---|---|---|
getElementById | #id only | 🚀 Ultra fast | Single element |
getElementsByClassName | .class only | ⚡ Fast | Live collection |
querySelector | Any CSS | 🟢 Moderate | First element |
querySelectorAll | Any CSS | 🟡 Slower | Static NodeList |
Decision tree:
I want to find an element...
├── I have an ID → getElementById
├── I have a class name only → getElementsByClassName (faster, but live)
├── I need flexible CSS selector → querySelector / querySelectorAll
└── I need to scope search to a subtree → parentEl.querySelector(...).closest(selector) — Walk UP the treeStarting from the element itself, travels UP through ancestors until it finds one that matches the selector. Returns null if no match is found.
// Given this HTML:
// <ul id="todo-list">
// <li data-id="3">
// <span>Buy milk</span>
// <button class="delete-btn">✕</button>
// </li>
// </ul>
document.querySelector("#todo-list").addEventListener("click", (e) => {
// User clicked the ✕ button — but we need the <li>'s data-id
const listItem = e.target.closest("li");
if (listItem) {
console.log("Delete item:", listItem.dataset.id); // "3"
}
});This is the #1 tool for event delegation — find the right ancestor no matter what was clicked.
| Returns | Use when |
|---|---|
Nearest matching ancestor Element or null | Event delegation, finding the wrapping component |
.parentElement — Direct parentconst child = document.querySelector(".child");
const parent = child.parentElement; // One level up
const grandparent = child.parentElement.parentElement; // Two levels up.children vs .childNodes — Direct childrenconst list = document.querySelector("ul");
list.children; // HTMLCollection — only <li> elements (no text nodes)
list.childNodes; // NodeList — EVERYTHING including whitespace text nodes
// Use .children 99% of the time
[...list..nextElementSibling / .previousElementSiblingconst secondItem = document.querySelector("li:nth-child(2)");
secondItem.nextElementSibling; // The third <li>
secondItem.previousElementSibling; // The first <li>addEventListener(type, callback, options) — The modern way to listenNever use el.onclick = ... in production — it can only store one handler and overwrites any previous one.
const btn = document.querySelector("#submit");
// ✅ Multiple listeners can be added safely
btn.addEventListener("click", handleSubmit);
btn.addEventListener("click", trackAnalytics);
// Remove when done (use the exact same function reference)
btn.removeEventListener("click", handleSubmit);Common event types: click, input, change, submit, keydown, keyup, focus, blur, mouseover, scroll, resize
e.target vs e.currentTargetdocument.querySelector("ul").addEventListener("click", (e) => {
console.log(e.target); // The actual element that was clicked (e.g., the <li>)
console.log(e.
The problem: If a list has 1000 items and you attach a click listener to each one, you're creating 1000 listeners in memory. Slow to set up, and dynamic items added later won't have listeners.
The solution — delegate to the parent:
const list = document.querySelector("#product-list");
list.addEventListener("click", (e) => {
// Check if user clicked a delete button (or something inside it)
const deleteBtn = e.target.closest
One listener handles clicks for ALL items — even items dynamically added to the list later.
e.preventDefault() vs e.stopPropagation()// preventDefault — stop the browser's DEFAULT behavior
document.querySelector("a#logo").addEventListener("click", (e) => {
e.preventDefault(); // Don't navigate to the href
scrollToTop(); // Do this instead
}
textContent vs innerHTML — The security divideconst div = document.querySelector("#output");
const userInput = '<script>alert("hacked!")</script>';
// ❌ DANGEROUS — executes any HTML/script in the string
div.innerHTML = userInput; // Alert fires!
// ✅ SAFE — treats everything as plain text
div.textContent = userInput; // Shows the literal string as textCross-Site Scripting (XSS) attacks happen when attacker-controlled HTML is injected into the page. Always use textContent for user-provided data. Only use innerHTML for hard-coded, trusted HTML strings.
When you must insert HTML dynamically, build elements programmatically:
function createUserCard(user) {
const card = document.createElement("div");
card.className = "user-card";
const name = document.createElement("h3");
classList API — Modern class managementconst el = document.querySelector(".sidebar");
el.classList.add("open"); // Add a class
el.classList.remove("hidden"); //
dataset — Custom data-* attributes<button data-user-id="42" data-action="delete" data-role="admin">Delete</button>const btn = document.querySelector("button");
// Read — camelCase in JS matches kebab-case in HTML
btn.dataset.userId; // "42" (data-user-id)
btn.dataset.action; // "delete"
createElement, append, prepend, before, after, remove// Create elements safely
const toast = document.createElement("div");
toast.className = "toast";
toast.textContent = "Saved successfully!";
// Insert positions
document
useRef Escape HatchReact manages the DOM for you. Use ref only when you need direct DOM access — like focusing an , measuring an element, or integrating a third-party library.
function SearchInput() {
const inputRef = useRef(null);
const listRef = useRef(null);
// Programmatic focus — can't be done with state
const focus = () => inputRef.current?.focus();
// Measure element size — can't be done with state
const getHeight = () => listRef.current?.getBoundingClientRect().height;
return (
<>
<input ref={inputRef} type="search" />
<ul ref={listRef}>{/* items */}</ul>
<button onClick={focus}>Focus Search</button>
</>
);
}Never do: add/remove elements via a ref. React's diffing engine won't know and your UI will break.