Regex, dates & browser APIs
Four unrelated toolboxes every real app ends up reaching for.
Regex — the essentials
/abc/ // literal — matches "abc" exactly
/abc/i // flag: i = case-insensitive
/abc/g // flag: g = find ALL matches, not just the first
/(\w+)@(\w+)/ // ( ) = a capturing group — grabbed separately from the full match
const text = "contact: ana@example.com today";
const match = text.match(/(\w+)@(\w+)\.com/);
console.log(match[0]); // the whole match — what happens?
console.log(match[1]); // group 1 — what happens?
console.log(match[2]); // group 2 — what happens?
"ana@example.com", then "ana", then
"example" — the full match is always index 0, and every
parenthesized group after it fills in one more slot, in order.
// Named groups — same idea, readable by name instead of position
const parsed = "2024-01".match(/(?<year>\d{4})-(?<month>\d{2})/);
parsed.groups.year; // "2024"
parsed.groups.month; // "01"
"2024-01-15".replace(/(\d+)-(\d+)-(\d+)/, "$3/$2/$1"); // "15/01/2024" — $1/$2/$3 refer back to the groups
[..."a1 b22 c333".matchAll(/[a-z](\d+)/g)].map((m) => m[1]); // ["1", "22", "333"] — every match, not just the first
.test() and .exec() on a regex literal
with the g flag mutate the regex object's own
lastIndex — the next call resumes searching from there,
not from the start of the string.
const stateful = /\d/g;
console.log(stateful.test("a1")); // what happens?
console.log(stateful.test("a1")); // SAME regex, same string — what happens?
console.log(stateful.test("a1")); // what happens?
true, false, true — alternating,
on the exact same input. First call finds the digit and leaves
lastIndex at 2; second call starts
searching from index 2 in a 2-character string, finds
nothing, and resets lastIndex back to 0;
third call starts over and finds it again. Reusing one global-flagged
regex object across unrelated calls is exactly how this bites — a
fresh /\d/g literal each time, or dropping the
g flag for a one-shot .test(), avoids it.
Dates
const d = new Date(2024, 0, 15); // year, MONTH (0-indexed!), day
console.log(d.getMonth()); // what happens?
console.log(d.getDate()); // what happens?
0, then 15 — getMonth() is
January-is-0, a decision baked into
Date since the original Java date API it was modeled on
in 1995, and never fixed since without breaking every existing
script.
const start = new Date("2024-01-15");
const end = new Date("2024-02-15");
(end - start) / 86_400_000; // 31 — subtracting Dates gives milliseconds; divide to get days
+ 86400000 — a DST boundary can make that arithmetic
land on the wrong calendar day entirely. This is the real reason
libraries like date-fns/Temporal (the
successor API, still stabilizing) exist: not laziness, a
correctness problem that's easy to get subtly wrong by hand.
new Intl.DateTimeFormat("en-IN", { dateStyle: "long" }).format(d);
// "15 January 2024" — locale-correct formatting, no manual string building
new Intl.NumberFormat("en-IN", { style: "currency", currency: "INR" }).format(150000);
// "₹1,50,000.00" — Indian digit grouping, handled for you
Browser storage
localStorage |
sessionStorage |
|
|---|---|---|
| Survives | closing the tab, the browser, the computer restarting | only this tab; gone when it closes |
| Shared across tabs? | yes, same origin | no — each tab gets its own |
| Capacity | ~5-10MB, string values only | same |
localStorage.setItem("theme", "dark");
localStorage.getItem("theme"); // "dark" — always a string
localStorage.setItem("user", JSON.stringify({ name: "Ana" }));
JSON.parse(localStorage.getItem("user")); // objects need to round-trip through JSON yourself
localStorage.removeItem("theme");
URL and URLSearchParams
const url = new URL("https://shop.example.com/search?q=js&page=2");
console.log(url.pathname); // what happens?
console.log(url.searchParams.get("q")); // what happens?
url.searchParams.set("page", "3");
console.log(url.toString()); // what happens?
A parsed URL gives every piece
(pathname, hostname, protocol)
as its own property, and searchParams is a live,
mutable view — editing it and reading url.toString()
again reflects the change immediately, no manual query-string
concatenation required.
History and IntersectionObserver, briefly
history.pushState({ page: 2 }, "", "/products?page=2"); // changes the URL bar, no page reload
window.addEventListener("popstate", (e) => {
console.log("back/forward pressed, state:", e.state); // fires on browser back/forward, not on pushState itself
});
This is the mechanism every client-side router (React Router, Next.js's own routing) is built on — a URL that changes without a real navigation, plus a way to hear when the user manually goes back or forward.
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) console.log(entry.target, "scrolled into view");
});
});
document.querySelectorAll(".lazy-image").forEach((img) => observer.observe(img));
The standard, efficient way to know when an element enters or leaves
the viewport — infinite scroll, lazy-loaded images, and "animate in
on scroll" effects all run on this instead of a
scroll listener doing math on every single pixel of
scrolling.
Events, in depth — live
Click the innermost box below and watch the log. Every listener here
is a real addEventListener call against the actual
nested boxes on this page.
With the capture checkbox off (the default), clicking "inner" logs
inner → middle → outer — the event starts at the exact
element clicked and bubbles upward through every ancestor
listening for it. Check the capture box and it reverses to
outer → middle → inner — capture-phase listeners run on
the way down, before the event even reaches its target.
Check "stopPropagation" and only the inner listener fires at all —
the click never continues past it in either direction.
Delegation and custom events
Bubbling is what makes event delegation work: one listener on
a parent container, instead of one per child, checking
event.target to see which child was actually clicked.
list.addEventListener("click", (e) => {
const item = e.target.closest("li"); // works even if the click landed on a span INSIDE the li
if (!item) return;
console.log("clicked:", item.dataset.id);
});
// one listener handles every current AND future <li> — no re-binding when items are added later
const updated = new CustomEvent("cart:updated", { detail: { count: 3 } });
cartElement.dispatchEvent(updated);
cartElement.addEventListener("cart:updated", (e) => {
console.log("new count:", e.detail.count);
});
A custom event bubbles and can be listened for exactly like a real browser event — the standard way for one part of a page to announce something happened without being directly wired to whoever might care.
Opens in the editor — write it, run it, and check it against real tests.