Stage 3 — JavaScript in the browser
The DOM, events, fetch, async/await, localStorage — every browser primitive you'll ever use in React.
Stage 3 — JavaScript in the browser
Time budget: ~2–3 weeks
In one line: Make web pages interactive — the DOM is the bridge between your JS code and your HTML.
Stage 1 taught you JavaScript as a language. This stage teaches it as a way to make web pages interactive. The bridge between your JS code and your HTML is the DOM (Document Object Model) — a tree representation of the page that your JS can read and change.
For the deeper "what is actually running here" picture, see Browser as a Runtime and Rendering Pipeline.
1. Loading JavaScript into a page
<!-- in index.html, right before </body> -->
<script src="app.js"></script>
<!-- modern: a module, can use import/export -->
<script type="module" src="app.js"></script>
Put <script> tags at the end of <body> so the HTML is parsed before your JS runs. Or use type="module" (or defer) and put it in the head — they wait for the DOM automatically.
2. Selecting elements: querySelector
// pass any CSS selector — same syntax as your CSS
const button = document.querySelector("#submit");
const firstP = document.querySelector("p");
const allCards = document.querySelectorAll(".card"); // NodeList of all matches
3. Listening for events
button.addEventListener("click", () => {
console.log("clicked!");
});
// event object — access the keyboard event, the target, etc.
input.addEventListener("keydown", (event) => {
if (event.key === "Enter") submit();
});
// form submission — preventDefault stops the page reload
form.addEventListener("submit", (event) => {
event.preventDefault();
const formData = new FormData(form);
console.log(formData.get("email"));
});
Common event names: click, submit, input (fires on every keystroke), change (fires when the value is committed), keydown, mouseenter, scroll.
Try it live — a real page with your JS running. Click the button and type in the box, then edit the code and watch the preview re-run:
4. Changing the page
title.textContent = "New title"; // safe text only
container.innerHTML = `<p>Hello</p>`; // ⚠ injects HTML — XSS risk if user data
element.classList.add("is-active");
element.classList.toggle("is-open");
element.setAttribute("disabled", "");
element.style.color = "red"; // inline style; usually toggle a class instead
// creating new elements
const li = document.createElement("li");
li.textContent = "New todo";
list.appendChild(li);
Prefer textContent over innerHTML when you're inserting user-supplied data — it can't be tricked into running scripts.
The whole loop, end-to-end:
Your JS doesn't touch HTML strings — it touches the DOM, the in-memory tree the browser built from the HTML. Every change you make to the DOM is what the user sees on the next repaint.
5. Asynchrony: promises and async/await
Things that take time — network requests, timers, file reads — return promises: an object representing "a value that will arrive later." You wait for them with await inside an async function.
async function loadUser() {
try {
const response = await fetch("https://api.github.com/users/octocat");
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const user = await response.json();
console.log(user.name);
} catch (err) {
console.error("failed:", err);
}
}
loadUser();
The mental model: await "pauses" the function until the promise settles. Other code keeps running in the meantime — the page doesn't freeze. fetch is the built-in way to make HTTP requests from the browser; .json() parses the response body as JSON.
6. localStorage — saving data on the user's device
localStorage.setItem("theme", "dark");
const theme = localStorage.getItem("theme"); // "dark" or null
// to store objects, JSON-serialise them
localStorage.setItem("todos", JSON.stringify(todos));
const todos = JSON.parse(localStorage.getItem("todos") || "[]");
Persists across page reloads but lives only in this browser on this device. Don't store secrets here — the user (and any browser extension) can read it.
7. DevTools — your most-used tool
Press F12 (or right-click → Inspect). Tabs to learn:
- Elements — inspect the DOM live, edit HTML/CSS in-place to experiment.
- Console — see
console.logoutput, type ad-hoc JS to inspect variables. - Network — see every request the page made, the response, the timing.
- Sources — set breakpoints, step through code with a real debugger.
- Application → Local Storage — view what your code is saving.
Senior devs live in DevTools. Learn it now, save a thousand hours later.
Where to go deeper
- javascript.info — Part 2 ("Browser: Document, Events, Interfaces"). Same caliber as Part 1.
- MDN Client-side Web APIs — the reference for everything the browser exposes.
- Chrome DevTools docs — the actual product docs. Underrated.
Deeper in this guide
- Browser as a Runtime — the event loop, the DOM, and what's actually executing your code.
- Rendering Pipeline — from HTML/CSS/JS to pixels on screen.
Project
In a stage-3/ folder, build a todo list as a single HTML file + a single JS file. Features: input + button to add a todo, click a todo to mark it done (struck-through), a delete button per todo, count of remaining items at the bottom, persistence via localStorage so reloading keeps your list. No frameworks. Then add a second feature: a button that calls the GitHub API (https://api.github.com/users/yourname) and shows your avatar and bio at the top of the page. By the end you'll have wired up DOM manipulation, events, async/await, fetch, JSON, and persistence — every browser primitive you'll ever use in React.
Common mistakes
- Putting
<script>in the<head>withoutdeferortype="module". The browser runs the script before parsing the body —document.querySelector("#submit")returnsnullbecause the element doesn't exist yet. Either put the tag right before</body>or adddefer/type="module". - Using
innerHTMLwith user-supplied data. Anything a user typed (a comment, a username, a search query) injected viainnerHTMLcan execute<script>tags — that's the XSS attack. UsetextContentfor text; reserveinnerHTMLfor static, developer-controlled strings. - Forgetting
event.preventDefault()on form submit. Without it, the form submits as a real HTTP request and the page reloads — your handler's "results" vanish in the reload. Always start a custom submit handler withe.preventDefault(). - Storing secrets in
localStorage. Auth tokens, API keys, anything sensitive — every browser extension and every line of your own JS can read localStorage. It's for preferences and drafts, not security.