Frontend Developer — Concept Notes
Concept-first, in depth. Examples use plain JS and React, but the ideas apply to any frontend stack. Print this page for a PDF.
1. What Frontend Really Is
The frontend is everything that runs on the user's device — code you ship but don't control. It is downloaded over a network you can't predict, executed on hardware you can't choose, and read by people using a screen reader, a 5-year-old Android, or a keyboard only.
Core responsibilities
- Structure — meaningful HTML that describes what content is, not how it looks.
- Presentation — CSS that stays predictable as the project grows.
- Behaviour — JavaScript that reacts to input without blocking the page.
- State management — keeping the UI and the data in sync; this is where most bugs live.
- Performance — bytes shipped, time to interactive, smooth scrolling.
- Accessibility — usable by keyboard, screen reader, and low vision users.
- Resilience — the network fails, APIs return 500, users double-click. Handle it.
Key mental model — the render lifecycle
Almost every frontend performance question reduces to: how do I do less work in this pipeline, or move it off the critical path?
2. How the Browser Works
The pieces
- Rendering engine — Blink (Chrome, Edge), WebKit (Safari), Gecko (Firefox). Parses HTML/CSS, lays out and paints.
- JS engine — V8, JavaScriptCore, SpiderMonkey. Parses and JIT-compiles JS.
- Browser process vs renderer process — each tab typically gets its own sandboxed renderer, so one crashed tab doesn't kill the browser.
- The main thread does parsing, layout, paint, and your JavaScript. That's why a long loop freezes scrolling.
Critical rendering path
The browser cannot paint until it has both DOM and CSSOM. So CSS is render-blocking by default, and a synchronous <script> in the head blocks parsing entirely.
| Script tag | Downloads | Executes | Use for |
|---|---|---|---|
<script> | Immediately, blocking parse | Immediately | Almost never in head |
async | Parallel | As soon as ready, out of order | Independent scripts (analytics) |
defer | Parallel | After parsing, in order | Your app code — the default choice |
type="module" | Parallel | Deferred by default | ES module entry points |
Reflow vs repaint vs composite
- Reflow (layout) — geometry changed:
width,top,font-size, adding a node. Expensive, can cascade to the whole document. - Repaint — pixels changed but geometry didn't:
color,background,box-shadow. Cheaper. - Composite only —
transformandopacitycan be handled by the GPU without touching layout or paint. This is why you animate transform, not top/left.
Layout thrashing
Reading a layout property (offsetHeight, getBoundingClientRect) forces the browser to flush pending style changes so the value is correct. Interleaving reads and writes in a loop forces a reflow every iteration.
// Bad — read/write/read/write forces N reflows
items.forEach(el => { el.style.height = el.offsetHeight + 10 + "px"; });
// Good — batch all reads, then all writes
const heights = items.map(el => el.offsetHeight);
items.forEach((el, i) => { el.style.height = heights[i] + 10 + "px"; });
3. HTML & Semantics
Semantic HTML means choosing the element that describes the meaning of the content. It is free accessibility, free SEO, and free default behaviour.
Structural elements
| Element | Meaning |
|---|---|
<header> / <footer> | Intro / closing content for its nearest section or the page |
<nav> | A block of major navigation links |
<main> | The primary content. One per page — screen readers can jump to it |
<article> | Self-contained and independently distributable (a post, a card) |
<section> | A thematic grouping — should have a heading |
<aside> | Tangential content (sidebar, callout) |
<figure>/<figcaption> | Media with a caption tied to it |
<time datetime> | A machine-readable date |
<button> over <div onclick>? A button is focusable, fires on Enter and Space, is announced as "button" by screen readers, can be disabled, and participates in forms. Recreating all of that on a div takes tabindex, two key handlers, and role="button" — and you'll still miss something.The document head
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Page title — Site</title>
<meta name="description" content="Shown in search results">
<meta property="og:title" content="Shown when shared on social">
<link rel="canonical" href="https://site.com/page">
<link rel="preconnect" href="https://api.site.com">
Without the viewport meta tag, mobile browsers render at ~980px wide and zoom out — your media queries will look like they don't work.
Images
alt=""(empty) for decorative images — this hides them from screen readers, which is correct. Omittingaltentirely makes the reader announce the filename.widthandheightattributes let the browser reserve space and prevent layout shift (CLS).loading="lazy"for below-the-fold images; never for the hero image.srcset/<picture>serve different sizes or formats (AVIF/WebP with a JPEG fallback).
<img src="hero-800.jpg" alt="Team at work"
srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1600.jpg 1600w"
sizes="(max-width: 700px) 100vw, 800px"
width="800" height="450">
4. Forms & Inputs
The label is not optional
<label for="email">Email</label>
<input id="email" name="email" type="email" required autocomplete="email">
A correctly associated label is announced by screen readers and expands the click target. A placeholder is not a label — it disappears as soon as the user types.
Input types earn you behaviour for free
type="email",tel,number,url— change the mobile keyboard.type="search",date,color,range,file— native widgets.required,min,max,pattern,minlength— native validation and the:invalidpseudo-class.autocomplete— a real UX win; browsers fill addresses and cards correctly when you use the standard tokens.
Controlled vs uncontrolled
An uncontrolled input keeps its value in the DOM; you read it on submit. A controlled input keeps its value in JS state and re-renders on every keystroke. Controlled gives you instant validation and derived UI; uncontrolled is cheaper and simpler for big forms.
// React — controlled
const [email, setEmail] = useState("");
<input value={email} onChange={e => setEmail(e.target.value)} />
// React — uncontrolled
const ref = useRef();
<input ref={ref} defaultValue="" /> // read ref.current.value on submit
Submitting properly
form.addEventListener("submit", async (e) => {
e.preventDefault(); // stop the full page reload
const data = Object.fromEntries(new FormData(form));
btn.disabled = true; // stop double submits
try { await api.save(data); }
finally { btn.disabled = false; }
});
<form> with a type="submit" button even in a SPA — you get Enter-to-submit, native validation, and password manager support for free.5. The Cascade & Specificity
When two rules target the same element, the browser resolves the conflict in this order:
- Origin & importance — author
!importantbeats author normal, which beats user-agent defaults. - Specificity — a score per selector.
- Source order — the last matching rule wins.
Specificity scoring (a, b, c)
| Selector | Score | Note |
|---|---|---|
#id | 1,0,0 | Very hard to override — avoid in component CSS |
.class, [attr], :hover | 0,1,0 | The sweet spot |
div, ::before | 0,0,1 | Element and pseudo-element |
*, :where() | 0,0,0 | :where() always scores zero |
inline style="" | above all selectors | Only !important beats it |
Compare left to right: 0,1,0 beats 0,0,9. Ten element selectors never beat one class.
.card .title { color: blue; } /* 0,2,0 — wins */
#page .title { color: red; } /* 1,1,0 — actually this wins */
:where(#page) .title { color: green; } /* 0,1,0 — :where() zeroes the id */
Inheritance
Text-ish properties inherit (color, font-*, line-height, visibility). Box properties do not (margin, padding, border, background). You can force either with inherit, initial, unset, or revert.
!important is a smell: it wins the cascade but takes the tool away from the next person — the only way to beat it is another !important. Fix the specificity instead. Legitimate uses: utility classes and overriding third-party CSS you can't edit.6. The Box Model & Sizing
Every element is a box: content → padding → border → margin.
box-sizing
content-box(default) —widthsets the content only; padding and border are added on top. A 200px box with 20px padding renders 240px wide.border-box—widthincludes padding and border. 200px means 200px.
*, *::before, *::after { box-sizing: border-box; } /* set this once, always */
Margin collapsing
Adjacent vertical margins between block elements merge into the largest of the two — 30px below + 20px above = 30px gap, not 50px. Horizontal margins never collapse. Collapsing is prevented by padding, a border, overflow other than visible, or a flex/grid container.
Units
| Unit | Relative to | Good for |
|---|---|---|
px | Nothing | Borders, fine detail |
rem | Root font-size | Spacing and type — respects user zoom settings |
em | Own font-size | Padding that scales with its own text; compounds when nested |
% | Parent's same dimension | Fluid widths |
vw / vh / dvh | Viewport | Full-screen sections; dvh handles mobile browser chrome |
ch | Width of "0" | Readable line length — max-width: 65ch |
fr | Free space in a grid | Grid tracks only |
rem for font sizes. If a user sets a larger default font for readability, px ignores them and rem respects them.7. Layout: Normal Flow & Positioning
display
block— full width, stacks vertically, respects width/height.inline— flows with text; width, height and vertical margin are ignored.inline-block— flows inline but accepts box dimensions.flex/grid— establish a new layout context for children.none— removed from the tree entirely (vsvisibility: hidden, which keeps the space, andopacity: 0, which keeps the space and stays clickable).
position
| Value | Positioned relative to | In flow? |
|---|---|---|
static | Nothing (default) | Yes |
relative | Its own normal position | Yes — original space kept |
absolute | Nearest positioned ancestor | No |
fixed | The viewport | No |
sticky | Scroll position, within its parent | Yes |
position: sticky needs a threshold (top: 0) and only sticks inside its parent's box — if it isn't working, the parent usually has overflow: hidden or no height.
Stacking contexts & z-index
z-index only compares elements within the same stacking context. A new context is created by: a positioned element with a z-index, opacity < 1, transform, filter, will-change, or a flex/grid child with a z-index.
z-index: 9999 and still hides behind the header." The modal's ancestor has transform or opacity, creating a context — so 9999 only competes inside that box. Fix: move the modal to a portal at the body root.Floats and overflow
Floats are legacy layout but still appear for wrapping text around an image. Modern replacement for clearfix: display: flow-root on the parent, which contains floats without hacks.
8. Flexbox
Flexbox is one-dimensional: you lay out along a main axis, and the cross axis is handled for you. Reach for it for toolbars, cards in a row, centring, and anything where content size should drive the layout.
Container properties
.container {
display: flex;
flex-direction: row | row-reverse | column | column-reverse;
flex-wrap: nowrap | wrap;
justify-content: flex-start | center | space-between | space-around | space-evenly;
align-items: stretch | flex-start | center | baseline; /* cross axis */
align-content: /* spacing of multiple lines — only with wrap */;
gap: 16px; /* use this, not margins */
}
Remember: justify-content works on the main axis, align-items on the cross axis. Change flex-direction to column and the two swap meaning.
Item properties
.item {
flex-grow: 0; /* share of leftover space to absorb */
flex-shrink: 1; /* how eagerly to shrink when space is tight */
flex-basis: auto; /* starting size before grow/shrink */
flex: 1; /* shorthand for 1 1 0% — equal columns */
flex: auto; /* 1 1 auto — sized by content, then grows */
align-self: center;/* override align-items for one item */
order: 2; /* visual order only — does NOT change tab order */
}
flex: 1 vs flex: auto: flex: 1 sets basis to 0, so items end up equal width regardless of content. flex: auto starts from content width, so a longer item stays wider. Interviewers ask this a lot.Two things that bite
- A flex item's
min-widthdefaults toauto, so it refuses to shrink below its content — long text or a wide<pre>overflows the container. Fix withmin-width: 0. - Perfect centring is just
display:flex; justify-content:center; align-items:center;.
9. Grid
Grid is two-dimensional — you define rows and columns and place items into them. Use it for page structure and any real grid of content.
.grid {
display: grid;
grid-template-columns: 200px 1fr 1fr; /* fixed sidebar + fluid */
grid-template-rows: auto 1fr auto;
gap: 20px;
}
/* Responsive with no media queries at all */
.cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 16px;
}
auto-fill keeps empty tracks; auto-fit collapses them so the items stretch to fill the row. minmax(250px, 1fr) means "at least 250px, otherwise share the space".
Placing items
.hero { grid-column: 1 / 3; } /* from line 1 to line 3 */
.hero { grid-column: span 2; } /* same thing, relative */
.full { grid-column: 1 / -1; } /* full width, whatever the count */
/* Named areas — very readable for page layout */
.page {
display: grid;
grid-template-columns: 240px 1fr;
grid-template-areas:
"sidebar header"
"sidebar main"
"sidebar footer";
}
.sidebar { grid-area: sidebar; }
Grid vs Flexbox
| Flexbox | Grid | |
|---|---|---|
| Dimensions | One axis at a time | Rows and columns together |
| Driven by | Content | The layout you declare |
| Best for | Navbars, button rows, centring | Page shells, card grids, dashboards |
They compose: a grid for the page, flexbox inside each cell.
10. Responsive Design
Mobile first
Write the small-screen styles as the base, then add complexity with min-width queries. It keeps the CSS additive instead of a pile of overrides.
.grid { display: grid; gap: 16px; } /* 1 column by default */
@media (min-width: 640px) { .grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1024px) { .grid { grid-template-columns: repeat(3, 1fr); } }
Fluid type and space
h1 { font-size: clamp(1.75rem, 1.2rem + 2.5vw, 3rem); }
/* min preferred(scales) max */
.section { padding-inline: max(1rem, 5vw); }
clamp(), min() and max() replace whole stacks of media queries.
Container queries
Media queries ask about the viewport; container queries ask about the parent — so a component can adapt wherever it's dropped.
.card-wrap { container-type: inline-size; }
@container (min-width: 400px) {
.card { display: grid; grid-template-columns: 120px 1fr; }
}
Other responsive concerns
- Touch targets of at least 44×44px.
@media (prefers-color-scheme: dark)and(prefers-reduced-motion: reduce)— respect both.100vhon mobile includes the browser chrome and causes jumping; prefer100dvh.- Test with the keyboard open and at 200% zoom, not just by dragging the window.
11. Transitions & Animation
.btn {
transition: background-color .2s ease, transform .2s ease;
}
.btn:hover { transform: translateY(-2px); }
@keyframes slide-in {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
.panel { animation: slide-in .3s ease-out both; }
Animate only cheap properties
Stick to transform and opacity — they are composited on the GPU and skip layout and paint entirely. Animating width, top, margin or box-shadow forces layout or paint every frame and drops you below 60fps.
| Instead of | Animate |
|---|---|
left / top | transform: translate() |
width / height | transform: scale() |
display: none | opacity + visibility, or @starting-style |
Respect motion preferences
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: .01ms !important;
transition-duration: .01ms !important;
}
}
This isn't a nicety — motion triggers nausea and migraines for some users.
CSS vs JS animation
CSS for state changes and simple sequences (it can run off the main thread). JS (Web Animations API, GSAP, Framer Motion) when you need to interrupt, chain, or drive animation from scroll or gesture position.
12. Modern CSS & Architecture
Custom properties
:root { --brand: #2f6f4e; --space: 8px; }
html[data-theme="dark"] { --brand: #7fc8a0; }
.btn { background: var(--brand); padding: calc(var(--space) * 2); }
Unlike Sass variables, these are live at runtime — they cascade, inherit, and can be read and set from JavaScript (el.style.setProperty("--brand", "#333")). This is how theme switching works with no rebuild.
Useful modern selectors
:is(h1, h2, h3) code— grouping; takes the highest specificity inside.:where(...)— same grouping but zero specificity; perfect for resets.:has(> img)— the long-awaited parent selector..card:has(img)styles cards that contain an image.:focus-visible— focus ring only for keyboard users, not mouse clicks. Never dooutline: nonewithout this.&native nesting,@layerfor cascade layers,aspect-ratio,inset: 0.
Naming and structure
| Approach | Idea | Trade-off |
|---|---|---|
| BEM | .card__title--large | Flat specificity, verbose names |
| Utility-first (Tailwind) | Compose from small classes | Very fast; markup gets noisy |
| CSS Modules | Hashed local class names | Real scoping, needs a build step |
| CSS-in-JS | Styles colocated in components | Dynamic; runtime cost unless compiled |
13. Types & Coercion
The seven primitives
string, number, boolean, null, undefined, symbol, bigint. Everything else — objects, arrays, functions, dates — is an object.
- Primitives are copied by value; objects are copied by reference.
typeof null === "object"— a 1995 bug kept for compatibility.typeof []is"object"; useArray.isArray().undefined= "not assigned yet";null= "deliberately empty".
Falsy values — memorise these
false, 0, -0, 0n, "", null, undefined, NaN. Everything else is truthy — including [], {}, "0" and "false".
== vs ===
0 == ""; // true — both coerced to 0
0 == "0"; // true
"" == "0"; // false — not transitive!
null == undefined // true — the one loose comparison worth using
null === undefined// false
NaN === NaN // false — use Number.isNaN()
[] == false // true — [] → "" → 0
Rule: always ===. The single exception is x == null to test for null-or-undefined in one go.
Copying
const a = { x: 1, nested: { y: 2 } };
const shallow = { ...a }; // nested is still shared
const deep = structuredClone(a); // real deep copy, built in
let n = 5; let m = n; m = 10; // n is still 5 (by value)
let o = {v:5}; let p = o; p.v = 10; // o.v is now 10 (by reference)
Nullish operators
const port = config.port ?? 3000; // only falls back on null/undefined
const port = config.port || 3000; // ALSO falls back on 0 — a bug
user?.profile?.name; // optional chaining, no TypeError
obj.method?.(); // call only if it exists
14. Scope, Hoisting & Closures
Declarations
var | let | const | |
|---|---|---|---|
| Scope | Function | Block | Block |
| Hoisted | Yes, as undefined | Yes, but in the TDZ | Same as let |
| Redeclare | Allowed | No | No |
| Reassign | Yes | Yes | No (binding, not the value) |
The Temporal Dead Zone is the gap between entering a scope and the let/const line — accessing the variable there throws a ReferenceError, which is far better than var's silent undefined.
const obj = {} then obj.a = 1 is legal: const freezes the binding, not the contents. Use Object.freeze() for the contents.
The classic loop question
for (var i = 0; i < 3; i++) setTimeout(() => console.log(i)); // 3 3 3
for (let i = 0; i < 3; i++) setTimeout(() => console.log(i)); // 0 1 2
var creates one binding shared by all three callbacks, which by the time they run holds 3. let creates a fresh binding per iteration, so each closure captures its own.
Closures
A closure is a function that keeps access to the variables of the scope where it was defined, even after that scope has returned. It's not magic — the function holds a reference to its lexical environment, so the garbage collector keeps it alive.
function counter() {
let count = 0; // private — not reachable from outside
return {
inc: () => ++count,
get: () => count,
};
}
const c = counter();
c.inc(); c.inc(); c.get(); // 2
Where closures show up in real work
// Debounce — run only after the user stops typing
function debounce(fn, delay = 300) {
let timer; // captured by the returned fn
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
// Throttle — run at most once per interval
function throttle(fn, limit = 300) {
let waiting = false;
return (...args) => {
if (waiting) return;
fn(...args);
waiting = true;
setTimeout(() => { waiting = false; }, limit);
};
}
15. this, call, apply, bind
this is decided by how a function is called, not where it is written. Check these rules in order:
new Fn()→thisis the newly created object.- Explicit —
fn.call(obj),fn.apply(obj),fn.bind(obj)→thisisobj. - Method call —
obj.fn()→thisisobj(whatever is left of the dot). - Plain call —
fn()→undefinedin strict mode / modules,windowotherwise.
Arrow functions ignore all of this. They have no this of their own and take it from the enclosing lexical scope, permanently. bind cannot change it.
const user = {
name: "Ada",
greetRegular() { console.log(this.name); },
greetArrow: () => console.log(this.name),
};
user.greetRegular(); // "Ada"
user.greetArrow(); // undefined — `this` is the module/window
const fn = user.greetRegular;
fn(); // undefined — lost the object on the way
fn.call(user); // "Ada"
const bound = fn.bind(user);
bound(); // "Ada" — permanently attached
The callback trap
class Timer {
constructor() { this.secs = 0; }
startBroken() { setInterval(function () { this.secs++; }, 1000); } // this = window
startOk() { setInterval(() => { this.secs++; }, 1000); } // arrow inherits
}
Same reason React class components needed this.handleClick = this.handleClick.bind(this) in the constructor — or a class field arrow function instead.
call vs apply vs bind
fn.call(ctx, a, b); // invoke now, args listed
fn.apply(ctx, [a, b]); // invoke now, args as an array
const g = fn.bind(ctx); // don't invoke — return a new bound function
16. Objects & Prototypes
The prototype chain
Every object has a hidden link ([[Prototype]], exposed as __proto__) to another object. When you read a property, JS walks up that chain until it finds it or hits null. That is the whole inheritance model — there are no classes underneath, just objects linking to objects.
const animal = { speak() { return `${this.name} makes a sound`; } };
const dog = Object.create(animal);
dog.name = "Rex";
dog.speak(); // found on animal, `this` is dog
Object.getPrototypeOf(dog) === animal; // true
Classes are syntax over prototypes
class Animal {
#secret = "private"; // true private field
static count = 0;
constructor(name) { this.name = name; Animal.count++; }
speak() { return `${this.name} makes a sound`; } // lives on Animal.prototype
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // must call before using `this`
this.breed = breed;
}
speak() { return `${super.speak()} — a bark`; }
}
Methods live on the prototype and are shared by every instance; fields defined in the constructor are copied per instance.
Useful object work
Object.keys(o); Object.values(o); Object.entries(o);
Object.fromEntries(pairs); // rebuild an object from entries
Object.assign(target, a, b); // shallow merge
Object.freeze(o); // shallow immutability
o.hasOwnProperty("k"); // own property, not inherited
Object.hasOwn(o, "k"); // the modern, safer form
const { a, b: renamed, c = 5, ...rest } = obj; // destructuring
const [first, , third = 0] = arr;
Map/Set vs object/array
- Map — any key type (including objects), preserves insertion order, has
.size, faster for frequent add/delete. - Set — unique values.
[...new Set(arr)]deduplicates in one line. - WeakMap/WeakSet — keys held weakly, so entries are collected when the key is garbage. Good for caching data against DOM nodes without leaking.
17. Arrays & Iteration
| Method | Returns | Mutates? |
|---|---|---|
map | New array, same length | No |
filter | New array, subset | No |
reduce | Any single value | No |
find / findIndex | First match / its index | No |
some / every | Boolean | No |
slice | A copy of a range | No |
splice | Removed items | Yes |
sort / reverse | The same array | Yes |
flat / flatMap | New flattened array | No |
at(-1) | Last element | No |
toSorted / toReversed | New array | No — modern, safe |
// reduce is just a loop with an accumulator
const total = items.reduce((sum, i) => sum + i.price, 0);
// group into an object
const byRole = users.reduce((acc, u) => {
(acc[u.role] ||= []).push(u);
return acc;
}, {});
// sort needs a comparator for numbers — default sort is lexicographic!
[10, 9, 1].sort(); // [1, 10, 9] ← surprise
[10, 9, 1].sort((a, b) => a - b); // [1, 9, 10]
// sort mutates — copy first in React state
const sorted = [...items].sort((a, b) => a.n - b.n);
Loop choices
for...of— values of any iterable; supportsbreakand works withawait.for...in— keys, including inherited ones. Avoid on arrays.forEach— cannotbreak, and ignores async callbacks (a very common bug).
// Bad — forEach doesn't await; "done" prints first
items.forEach(async i => { await save(i); });
console.log("done");
// Sequential
for (const i of items) await save(i);
// Parallel
await Promise.all(items.map(i => save(i)));
18. The Event Loop
JavaScript is single-threaded: one call stack, one thing at a time. Concurrency comes from the runtime handing slow work (timers, network, DOM events) to the browser and queuing the callback for later.
The interview question
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
queueMicrotask(() => console.log("4"));
console.log("5");
// Output: 1, 5, 3, 4, 2
Sync code first (1, 5). Then the stack empties and all microtasks drain (3, 4). Only then one macrotask runs (2). This is why setTimeout(fn, 0) is never actually 0 — it's "after the current work and all pending microtasks".
Why it matters practically
- A long synchronous loop blocks everything: no clicks, no scroll, no paint. Break work into chunks or move it to a Web Worker.
- An infinite microtask chain starves rendering completely — microtasks drain fully before the browser is allowed to paint.
requestAnimationFrameruns just before paint, which is the right place for visual updates.setTimeoutis clamped to ~4ms after several nested calls, and throttled hard in background tabs.
19. Promises & async/await
A promise is an object representing a value that isn't ready yet. It is in one of three states — pending, fulfilled, or rejected — and once settled it never changes again.
const p = new Promise((resolve, reject) => {
setTimeout(() => resolve("done"), 1000); // or reject(new Error("nope"))
});
p.then(v => v.toUpperCase()) // each .then returns a NEW promise
.then(v => console.log(v))
.catch(err => console.error(err))// catches any rejection above it
.finally(() => stopSpinner()); // runs either way
async/await is the same thing, readable
async function load(id) { // always returns a promise
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`); // fetch does NOT throw on 404/500
return await res.json();
} catch (err) {
console.error("Load failed:", err);
throw err; // rethrow, or the caller thinks it worked
}
}
Combinators
| Method | Resolves when | Rejects when |
|---|---|---|
Promise.all | All fulfil — array of values | Any rejects (fail fast) |
Promise.allSettled | All settle — {status, value/reason}[] | Never |
Promise.race | First to settle, either way | If the first settled one rejected |
Promise.any | First to fulfil | Only if all reject |
// Sequential — 3 seconds
const a = await getA(); const b = await getB(); const c = await getC();
// Parallel — 1 second. Use this whenever the calls are independent.
const [a, b, c] = await Promise.all([getA(), getB(), getC()]);
// Don't let one failure kill the page
const results = await Promise.allSettled([getA(), getB()]);
const ok = results.filter(r => r.status === "fulfilled").map(r => r.value);
fetch only rejects on a network failure — a 500 response is a fulfilled promise, so you must check res.ok yourself. (2) An unhandled rejection is a silent bug in the browser; always .catch or wrap in try/catch.20. Modules & Tooling
// named exports — many per file, imported by exact name
export const PI = 3.14;
export function area(r) { return PI * r * r; }
import { PI, area as circleArea } from "./math.js";
// default export — one per file, any name on import
export default function Button() {}
import Btn from "./Button.jsx";
// dynamic import — returns a promise, creates a separate chunk
const { Chart } = await import("./Chart.js");
ES modules are static (the imports are known before running), which is what makes tree shaking possible. They are also deferred and run in strict mode automatically.
ESM vs CommonJS
| ESM | CommonJS | |
|---|---|---|
| Syntax | import / export | require / module.exports |
| Loading | Static, async | Dynamic, synchronous |
| Tree shaking | Yes | No |
| Where | Browsers + modern Node | Legacy Node |
What a bundler does
- Bundle — follow the import graph into a few files, avoiding hundreds of requests.
- Transpile — Babel/SWC/esbuild convert modern JS and JSX to what browsers support.
- Tree shake — drop exports nobody imports.
- Code split —
import()and route boundaries become separate chunks loaded on demand. - Hash filenames —
app.8fa3b.jsso you can cache forever and bust on change.
Vite serves native ESM in dev (instant startup, real HMR) and bundles with Rollup for production. Webpack is the older, more configurable equivalent.
21. DOM Manipulation
document.querySelector(".card"); // first match, any CSS selector
document.querySelectorAll(".card"); // static NodeList — snapshot
document.getElementsByClassName("card");// LIVE HTMLCollection — updates itself
el.textContent = "safe"; // escapes everything — use this
el.innerHTML = userInput; // XSS risk — never with untrusted data
el.insertAdjacentHTML("beforeend", html);
el.classList.add("on"); el.classList.toggle("on", isOn);
el.dataset.userId = 42; // <div data-user-id="42">
el.setAttribute("aria-expanded", "true");
el.closest(".card"); // walk UP to the nearest match
input.getAttribute("value") returns the initial HTML value; input.value returns what the user has actually typed. They diverge the moment someone types.Creating nodes efficiently
// Each append to the live DOM can trigger layout — batch with a fragment
const frag = document.createDocumentFragment();
for (const item of items) {
const li = document.createElement("li");
li.textContent = item.name;
frag.append(li);
}
list.append(frag); // one DOM operation
Measuring
getBoundingClientRect()— size and position relative to the viewport.offsetWidthincludes padding and border;clientWidthexcludes the border;scrollWidthis the full content width.- Every one of these forces a synchronous layout — read them in a batch, never inside a write loop.
22. Events in Depth
The three phases
Listeners run in the bubble phase by default; pass { capture: true } for the way down. Most events bubble — focus, blur, load and scroll (on an element) do not. focusin/focusout are the bubbling versions of focus/blur.
Delegation
// One listener for a list of any size — works for items added later
list.addEventListener("click", (e) => {
const btn = e.target.closest("[data-id]");
if (!btn || !list.contains(btn)) return;
remove(btn.dataset.id);
});
This is the single most useful event pattern: fewer listeners, less memory, and it survives dynamic content.
stopPropagation vs preventDefault
e.preventDefault()— cancel the browser's default action (form submit, link navigation, checkbox toggle). The event still propagates.e.stopPropagation()— stop travelling to ancestors. The default action still happens.e.stopImmediatePropagation()— also stop other listeners on this same element.
e.target is what was actually clicked; e.currentTarget is the element the listener is attached to. Inside a delegated handler they differ, and that's the point.
Listener options
el.addEventListener("scroll", onScroll, { passive: true }); // never calls preventDefault
el.addEventListener("click", once, { once: true }); // auto-removes itself
const ac = new AbortController();
el.addEventListener("move", fn, { signal: ac.signal });
ac.abort(); // removes it — great for cleanup
{ passive: true } on scroll and touch listeners tells the browser it doesn't need to wait for your handler before scrolling — a real, measurable scroll performance win.
23. Storage & Cookies
| localStorage | sessionStorage | Cookies | IndexedDB | |
|---|---|---|---|---|
| Size | ~5–10MB | ~5–10MB | ~4KB | Hundreds of MB |
| Expires | Never | On tab close | You set it | Never |
| Sent to server | No | No | Every request | No |
| API | Sync, strings only | Sync, strings only | Sync, string | Async, structured |
| Use for | Theme, drafts | One-tab flow state | Sessions, auth | Offline data, files |
localStorage.setItem("theme", "dark");
JSON.parse(localStorage.getItem("user") ?? "null"); // always guard — may be null
// Wrap in try/catch: private mode and blocked site data can throw.
Cookie flags that matter
Set-Cookie: sid=abc123; HttpOnly; Secure; SameSite=Lax; Max-Age=604800; Path=/
- HttpOnly — JS cannot read it. This is what protects a session token from XSS.
- Secure — HTTPS only.
- SameSite —
Strict(never cross-site),Lax(sent on top-level navigation; the modern default),None(requires Secure). This is the main CSRF defence.
localStorage is readable by any injected script, so an XSS bug hands over the token. An HttpOnly cookie can't be read by JS but needs CSRF protection (SameSite + a CSRF token). The cookie is generally the safer default.24. Fetch, HTTP & CORS
const res = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(user),
credentials: "include", // send cookies cross-origin
signal: controller.signal, // for cancellation
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
Status codes worth knowing
| Code | Meaning |
|---|---|
| 200 / 201 / 204 | OK / Created / No content |
| 301 / 302 / 304 | Moved permanently / temporarily / Not modified (cached) |
| 400 / 401 / 403 | Bad request / Not authenticated / Authenticated but not allowed |
| 404 / 409 / 422 / 429 | Not found / Conflict / Unprocessable / Rate limited |
| 500 / 502 / 503 | Server error / Bad gateway / Unavailable |
401 vs 403: 401 means "I don't know who you are" (log in); 403 means "I know who you are and you still can't".
Cancelling
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.catch(e => { if (e.name !== "AbortError") throw e; });
controller.abort(); // cancel a stale search request or on unmount
CORS
The browser's same-origin policy blocks JS from reading a response from a different origin (scheme + host + port). CORS is the server opting in with response headers. Two important points: CORS is enforced by the browser, not the server — curl and Postman are unaffected — and the request usually still reached the server.
Access-Control-Allow-Origin: https://app.site.com
Access-Control-Allow-Credentials: true // cannot be used with Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
A preflight OPTIONS request is sent first when the request isn't "simple" — i.e. it uses a method other than GET/HEAD/POST, a custom header like Authorization, or a JSON content type. The browser asks permission before sending the real request.
Caching headers
Cache-Control: max-age=31536000, immutable— for hashed assets.Cache-Control: no-cache— cache, but revalidate every time (viaETag).ETag/If-None-Match→ a 304 with no body when nothing changed.
25. Browser APIs Worth Knowing
IntersectionObserver
Tells you when an element enters or leaves the viewport — without a scroll handler. Use it for lazy loading, infinite scroll, scrollspy, and reveal-on-scroll animation.
const io = new IntersectionObserver((entries) => {
entries.forEach(e => { if (e.isIntersecting) e.target.classList.add("visible"); });
}, { rootMargin: "0px 0px -20% 0px", threshold: 0.1 });
document.querySelectorAll(".reveal").forEach(el => io.observe(el));
Others
- ResizeObserver — react to an element's size changing, not just the window's.
- MutationObserver — watch for DOM changes (third-party widgets, rich text editors).
- Web Workers — run heavy JS on another thread so the UI stays responsive. No DOM access; you communicate by
postMessage. - Service Worker — a proxy between the page and the network: offline support, caching, push. The backbone of a PWA.
- History API —
pushState/popstate; this is how client-side routers work without reloading. - WebSocket — a persistent two-way connection for chat and live updates.
EventSource(SSE) is simpler when you only need server→client. - Clipboard, Geolocation, Notification, matchMedia, Page Visibility — permission-gated but simple.
// Pause polling when the tab is hidden — free battery and quota
document.addEventListener("visibilitychange", () => {
document.hidden ? stopPolling() : startPolling();
});
// Read a media query from JS, and react to changes
const mq = matchMedia("(prefers-color-scheme: dark)");
mq.addEventListener("change", e => setTheme(e.matches ? "dark" : "light"));
26. Mental Model & JSX
React's core idea: UI is a function of state. You never describe the steps to update the DOM — you describe what the UI should look like for the current state, and React works out the difference.
JSX is just function calls
<div className="card">Hi</div>
// compiles to
React.createElement("div", { className: "card" }, "Hi")
// which produces a plain object:
{ type: "div", props: { className: "card", children: "Hi" } }
That's why components must return a single root (or a <>fragment</>), why attributes are camelCase (className, onClick, htmlFor), and why {} holds any JS expression — but not statements like if.
The virtual DOM and reconciliation
The "virtual DOM" is just that tree of objects. Comparing two objects is cheap; touching the real DOM is expensive. React compares old and new trees with two assumptions: elements of a different type produce a completely new subtree, and children are matched by key.
Keys
{items.map((item, i) => <Row key={item.id} ... />)} // stable identity ✓
{items.map((item, i) => <Row key={i} ... />)} // index — bug source ✗
With an index key, deleting the first item makes React think every row changed its content rather than that one row was removed — so component state (an input's text, a checkbox) sticks to the wrong row. Index keys are only safe for a static list that is never reordered, filtered or added to.
key is also a deliberate tool: it unmounts and remounts the component, resetting all of its state. Handy for resetting a form when the selected record changes.27. State & Rendering
Props vs state
Props come from the parent and are read-only. State is owned by the component and changing it triggers a re-render. When two components need the same data, lift state up to their closest common parent.
State updates are asynchronous and batched
const [count, setCount] = useState(0);
setCount(count + 1);
setCount(count + 1); // both read the SAME stale count → +1 total
setCount(c => c + 1);
setCount(c => c + 1); // functional updates queue → +2 ✓
Use the functional form whenever the next value depends on the previous one.
Immutability is not optional
// ✗ React compares by reference — same object, no re-render
user.name = "New"; setUser(user);
items.push(newItem); setItems(items);
// ✓ new reference every time
setUser({ ...user, name: "New" });
setItems([...items, newItem]);
setItems(items.filter(i => i.id !== id));
setItems(items.map(i => i.id === id ? { ...i, done: true } : i));
// nested — spread at every level you change
setState(s => ({ ...s, address: { ...s.address, city: "Pune" } }));
When does a component re-render?
- Its own state changed.
- Its parent re-rendered (by default — children re-render regardless of props).
- A context it consumes changed.
Re-rendering is not the same as touching the DOM. React re-runs the function and diffs; if the output is identical, no DOM work happens. Don't optimise renders you haven't measured.
Derived state
// ✗ duplicated state that can drift out of sync
const [items, setItems] = useState([]);
const [count, setCount] = useState(0);
// ✓ just compute it during render
const count = items.length;
const visible = items.filter(i => !i.done);
28. Effects & Lifecycle
useEffect is for synchronising with something outside React — a network request, a subscription, a timer, the document title. It is not a general "run some code" hook.
useEffect(() => {
const id = setInterval(tick, 1000);
return () => clearInterval(id); // cleanup: unmount + before every re-run
}, []); // deps
| Deps | Runs |
|---|---|
| omitted | After every render |
[] | Once after mount (twice in dev StrictMode — deliberate) |
[a, b] | On mount and whenever a or b changes (Object.is comparison) |
Fetching with cleanup
useEffect(() => {
const ac = new AbortController();
(async () => {
try {
const res = await fetch(`/api/users/${id}`, { signal: ac.signal });
setUser(await res.json());
} catch (e) { if (e.name !== "AbortError") setError(e); }
})();
return () => ac.abort(); // stops a stale response overwriting a newer one
}, [id]);
Without the cleanup, switching quickly between ids can let a slow first response land after the fast second one — a race condition that shows the wrong user.
Common mistakes
- Missing deps — the effect closes over stale values. Let the lint rule guide you rather than trimming the array.
- An object or function in deps — recreated each render, so the effect runs every time. Wrap it in
useMemo/useCallbackor move it inside the effect. - Setting state in an effect that the effect depends on — an infinite loop.
- Using an effect to transform props into state — calculate it during render instead.
useLayoutEffect
Same API but runs synchronously after DOM mutation, before paint. Use it only when you must measure the DOM and adjust before the user sees anything (a tooltip position). It blocks paint, so it's the exception.
29. Hooks in Depth
The rules of hooks
Call hooks only at the top level of a component or another hook — never in a condition, loop or nested function. React tracks hooks by call order, so a conditional hook shifts every subsequent hook's state onto the wrong slot.
The core set
const [v, setV] = useState(initial); // or useState(() => expensive())
const [s, dispatch] = useReducer(reducer, init); // complex/related transitions
const ref = useRef(null); // DOM node, or a mutable box that
// survives renders WITHOUT re-rendering
const memo = useMemo(() => heavy(a), [a]); // cache a value
const cb = useCallback(() => fn(a), [a]); // cache a function identity
const theme = useContext(ThemeContext);
const id = useId(); // stable SSR-safe id for label/input
useRef has two jobs
const inputRef = useRef(null); // 1. reach a DOM node
<input ref={inputRef} />; inputRef.current.focus();
const renders = useRef(0); // 2. mutable value, no re-render
renders.current++; // changing .current never re-renders
useReducer when state moves together
function reducer(state, action) {
switch (action.type) {
case "loading": return { ...state, loading: true, error: null };
case "success": return { loading: false, error: null, data: action.data };
case "error": return { loading: false, error: action.error, data: null };
default: return state;
}
}
const [state, dispatch] = useReducer(reducer, { loading: false, data: null, error: null });
Three useStates that always change together is the signal to switch — it makes impossible states (loading and error) unrepresentable.
Custom hooks
A custom hook is just a function starting with use that calls other hooks. It shares logic, not state — two components calling useFetch get independent state.
function useDebounced(value, delay = 300) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const t = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(t);
}, [value, delay]);
return debounced;
}
// in a component
const query = useDebounced(input); // only fires a search when typing pauses
useEffect(() => { if (query) search(query); }, [query]);
30. Performance & Patterns
The memo trio
const Row = React.memo(function Row({ item, onSelect }) { ... });
// skips re-render when props are shallow-equal
const handleSelect = useCallback((id) => setSelected(id), []);
const sorted = useMemo(() => [...items].sort(byName), [items]);
React.memo alone is useless if you pass a fresh object or inline arrow as a prop — the shallow comparison fails every render. That's exactly why useCallback and useMemo exist: to keep prop identity stable.
Composition beats memoisation
// Every state change re-renders <Expensive />
function App() {
const [n, setN] = useState(0);
return <><button onClick={() => setN(n+1)}>{n}</button><Expensive /></>;
}
// Move state down, or pass the child in as children — it no longer re-renders
function App({ children }) {
const [n, setN] = useState(0);
return <><button onClick={() => setN(n+1)}>{n}</button>{children}</>;
}
Code splitting
const Dashboard = lazy(() => import("./Dashboard"));
<Suspense fallback={<Spinner />}><Dashboard /></Suspense>
Split at route boundaries first, then at heavy components (charts, editors, maps).
Patterns worth naming
- Container / presentational — separate data fetching from rendering.
- Compound components —
<Tabs><Tab/></Tabs>sharing state via context. - Render props / children as function — pass behaviour, let the caller render.
- Error boundaries — a class component with
componentDidCatch; still the only way to catch render errors. - Portals —
createPortal(modal, document.body)escapes overflow and stacking contexts. - Virtualisation — render only visible rows for lists in the thousands.
31. Data, Routing & Forms
Context is for low-frequency global state
const ThemeContext = createContext("light");
<ThemeContext.Provider value={theme}><App /></ThemeContext.Provider>
const theme = useContext(ThemeContext);
Context solves prop drilling, not performance: every consumer re-renders when the value changes, and an inline object literal as value creates a new reference every render. Good for theme, locale, and the current user. Bad for fast-changing state.
Choosing a state tool
| Kind of state | Use |
|---|---|
| Local UI (open, input) | useState |
| Complex related transitions | useReducer |
| Rarely-changing global | Context |
| Frequently-changing global | Zustand / Redux Toolkit |
| Server data | TanStack Query / SWR / RTK Query |
| Shareable filters, tabs, page | The URL |
useEffect is where most React apps accumulate bugs — a query library gives you caching, background refresh, and loading/error states for free.Routing
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="users/:id" element={<User />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
const { id } = useParams();
const [params, setParams] = useSearchParams();
const navigate = useNavigate();
Client-side routing uses the History API — no page reload, so state survives navigation. The server must be configured to return index.html for every path, or a refresh on /users/5 gives a 404.
CSR vs SSR vs SSG
| Rendered | Good for | Cost | |
|---|---|---|---|
| CSR | In the browser | Dashboards behind a login | Blank first paint, weak SEO |
| SSR | On the server per request | Personalised, SEO-sensitive pages | Server load, TTFB |
| SSG | At build time | Docs, blogs, marketing | Rebuild to update |
| ISR | Built, then revalidated | Large mostly-static catalogues | Some staleness |
Hydration is React attaching event listeners to server-rendered HTML. A "hydration mismatch" means the server and client rendered different markup — usually from Date.now(), Math.random(), or reading window during render.
32. Performance
Core Web Vitals
| Metric | Measures | Good | Main fixes |
|---|---|---|---|
| LCP Largest Contentful Paint | When the main content appears | < 2.5s | Optimise the hero image, preload it, cut render-blocking CSS/JS, faster TTFB |
| INP Interaction to Next Paint | Responsiveness to input | < 200ms | Break up long tasks, less JS, debounce, use a worker |
| CLS Cumulative Layout Shift | Unexpected movement | < 0.1 | width/height on images, reserve ad/embed space, font-display: swap + size-adjust |
Loading strategy
- Ship less JavaScript — the single biggest lever. Audit the bundle, drop heavy dependencies (moment → date-fns, lodash → native), code-split routes.
deferyour scripts;preloadthe hero image and critical font;preconnectto the API origin.- Images — modern formats (AVIF/WebP), correct dimensions,
srcset, lazy-load below the fold. - Fonts — subset, self-host,
font-display: swap, and preload the one used above the fold. - Caching — hashed filenames with a long
max-age; compress with Brotli.
Runtime performance
// Break a long task so the browser can paint and respond between chunks
async function processAll(items) {
for (let i = 0; i < items.length; i++) {
process(items[i]);
if (i % 100 === 0) await new Promise(r => setTimeout(r, 0));
}
}
- Virtualise long lists; debounce/throttle high-frequency handlers; use
{ passive: true }on scroll. - Animate only
transformandopacity. - Batch DOM reads and writes; prefer
IntersectionObserverover scroll math.
33. Accessibility
The four POUR principles: Perceivable, Operable, Understandable, Robust. In practice it comes down to a handful of habits.
The habits
- Semantic HTML first. A native
<button>,<a>,<input>or<dialog>gives you keyboard support, focus and screen-reader roles for free. - Keyboard — everything reachable with Tab, activatable with Enter/Space, dismissible with Escape. Never remove the focus ring without replacing it (
:focus-visible). - Contrast — 4.5:1 for normal text, 3:1 for large text and UI borders.
- Labels — every input has a real
<label>; every icon-only button hasaria-label. - Headings — one
h1, no skipped levels; screen reader users navigate by heading. - Don't rely on colour alone to convey meaning — add an icon or text.
ARIA — the rules
No ARIA is better than bad ARIA. ARIA changes only how assistive tech announces something; it adds no behaviour. Use it when no native element fits.
<button aria-expanded="false" aria-controls="menu">Menu</button>
<div id="menu" hidden>…</div>
<div role="alert">Saved</div> <!-- interrupts immediately -->
<div aria-live="polite">3 results</div> <!-- announced when idle -->
<button aria-label="Close">×</button>
<span aria-hidden="true">🔥</span> <!-- decorative -->
Modals — the checklist interviewers love
- Move focus into the dialog when it opens.
- Trap focus inside while it's open.
- Close on Escape and on backdrop click.
- Return focus to the trigger element on close.
role="dialog"aria-modal="true"and a labelled title — or just use<dialog>, which does most of this natively.
Test by unplugging your mouse, then with a real screen reader (VoiceOver on Mac: ⌘F5). Automated tools like axe catch roughly a third of issues — the rest is manual.
34. Frontend Security
XSS — Cross-Site Scripting
Attacker-controlled data ends up executed as code in your page. The consequence is total: read localStorage, make requests as the user, rewrite the page.
// ✗ any of these with untrusted input
el.innerHTML = comment;
<div dangerouslySetInnerHTML={{ __html: comment }} />
eval(input); new Function(input); <a href={userUrl}> // javascript: URLs
// ✓
el.textContent = comment; // escapes automatically
el.innerHTML = DOMPurify.sanitize(comment); // when you must render HTML
React escapes interpolated values by default, which prevents most XSS — dangerouslySetInnerHTML is named that way for a reason. Add a Content-Security-Policy header as defence in depth.
CSRF
A malicious site makes the user's browser send an authenticated request to yours, using cookies that are attached automatically. Defences: SameSite=Lax/Strict cookies, a CSRF token, and never using GET for state changes.
Other essentials
- No secrets in frontend code. Every env var in the bundle is public —
VITE_/NEXT_PUBLIC_prefixes exist to make that explicit. - Never trust the client. Hiding an admin button is UX; the server must still check permissions.
rel="noopener noreferrer"ontarget="_blank"links so the opened page can't touchwindow.opener.- Dependencies —
npm audit, lockfiles, and scepticism about adding packages. - HTTPS everywhere, plus
Strict-Transport-Security,X-Content-Type-Options: nosniffand a sensibleframe-ancestorsto prevent clickjacking.
35. Testing
| Level | Tests | Tools |
|---|---|---|
| Unit | One function or hook in isolation | Vitest, Jest |
| Component | Rendering and interaction | React Testing Library |
| Integration | Several units together, mocked network | RTL + MSW |
| E2E | A real browser, a real flow | Playwright, Cypress |
test("submits the entered email", async () => {
const onSubmit = vi.fn();
render(<SignupForm onSubmit={onSubmit} />);
await userEvent.type(screen.getByLabelText(/email/i), "a@b.com");
await userEvent.click(screen.getByRole("button", { name: /sign up/i }));
expect(onSubmit).toHaveBeenCalledWith({ email: "a@b.com" });
});
- Prefer
getByRole→getByLabelText→getByText;getByTestIdis the last resort. - Mock the network (MSW), not your own modules.
- Test behaviour and output, not internal state or implementation details.
- A small number of E2E tests covering the critical paths (signup, checkout) beats a large number of brittle ones.
Interview Q&A — quick answers
HTML & CSS
- Why semantic HTML? Accessibility, SEO and free default behaviour — the element describes the meaning, not the appearance.
- Explain the box model. content → padding → border → margin;
border-boxmakeswidthinclude padding and border. - Specificity order? inline > id > class/attribute/pseudo-class > element; ties break on source order;
!importantoverrides the lot. - Flexbox vs Grid? Flex is one-dimensional and content-driven; Grid is two-dimensional and layout-driven.
flex: 1vsflex: auto?1means basis 0 → equal widths;automeans basis auto → content-sized, then grows.- Centre a div?
display:grid; place-items:center;on the parent. - display:none vs visibility:hidden vs opacity:0? Removed from layout / keeps space, not interactive / keeps space, still clickable.
- position sticky? Relative until a scroll threshold, then fixed — but only within its parent's box.
- rem vs em? rem is relative to the root; em to the element's own font-size and compounds when nested.
JavaScript
- == vs ===? Loose equality coerces types; strict doesn't. Always use
===. - What is a closure? A function that retains access to its defining scope after that scope has returned. Powers private state, debounce, throttle, module pattern.
- var vs let vs const? Function-scoped and hoisted as undefined vs block-scoped with a TDZ;
constcan't be reassigned but its contents can change. - How does
thiswork? Determined by the call site: new → the instance, explicit bind/call/apply → that object, method call → the object before the dot, plain call → undefined/window. Arrow functions inherit lexically. - Explain the event loop. Stack → drain all microtasks → render → one macrotask → repeat. Promises are microtasks;
setTimeoutis a macrotask. - Output of
console.log(1); setTimeout(...0); Promise.then(...); console.log(2)? 1, 2, promise, timeout. - Prototypal inheritance? Objects link to other objects; property lookup walks the chain. Classes are syntax over it.
- Promise.all vs allSettled?
allrejects on the first failure;allSettledalways resolves with each outcome. - Debounce vs throttle? Debounce runs after the activity stops; throttle runs at most once per interval.
- Event delegation? One listener on a parent using
e.target.closest()— fewer listeners, works for dynamic children. - Shallow vs deep copy? Spread copies one level;
structuredClonegoes all the way down.
React
- Virtual DOM? A lightweight object tree; React diffs old against new and applies the minimum DOM changes.
- Why keys? Stable identity during reconciliation. Index keys break on reorder or deletion and can attach state to the wrong row.
- State vs props? State is owned and mutable via setter; props come from the parent and are read-only.
- Why is my state not updating immediately? Updates are batched and asynchronous — use the functional form
setX(x => …). - useEffect dependency array? Omitted → every render;
[]→ once on mount;[a]→ whenachanges. Always return cleanup for subscriptions and timers. - useMemo vs useCallback? Cache a computed value vs cache a function's identity.
useCallback(fn, d)≡useMemo(() => fn, d). - When does a component re-render? Own state changed, parent re-rendered, or a consumed context changed.
- Context vs Redux? Context passes values down without drilling (all consumers re-render); Redux/Zustand is for frequently-changing global state with selectors and devtools.
- Controlled vs uncontrolled input? Value in React state vs value in the DOM read via a ref.
- Custom hook? A
use…function composing other hooks; shares logic, not state. - Why does my effect run twice? StrictMode in development, deliberately — to expose missing cleanup.
Browser & general
- localStorage vs sessionStorage vs cookies? Persistent / per-tab / sent with every request and configurable via flags.
- What is CORS? A browser-enforced rule; the server opts in with
Access-Control-Allow-*headers. A preflight OPTIONS precedes non-simple requests. - defer vs async? Both download in parallel;
deferexecutes after parsing in order,asyncexecutes as soon as it lands, out of order. - Reflow vs repaint? Geometry recalculated vs pixels redrawn. Composite-only (
transform/opacity) is cheapest. - How do you prevent XSS?
textContentoverinnerHTML, sanitise any HTML you must render, add a CSP, keep tokens in HttpOnly cookies. - Improve page load? Ship less JS, code-split, optimise images and fonts, cache with hashed filenames, defer non-critical work — then measure.
- Core Web Vitals? LCP < 2.5s, INP < 200ms, CLS < 0.1.
Learning Path
- HTML & CSS properly — semantics, the box model, the cascade. Build three static pages from a design without a framework.
- Layout — Flexbox and Grid until you stop guessing. Rebuild a real site's layout from scratch.
- Responsive — mobile-first,
clamp(), container queries, and testing at 200% zoom. - JavaScript fundamentals — types, scope, closures,
this, prototypes, array methods. Don't skip to a framework here; everything later depends on it. - Async JS — the event loop, promises, async/await, error handling, cancellation.
- The DOM — build a to-do app and an image carousel with no library at all, once.
- Fetch & APIs — loading, error and empty states; CORS; a real REST integration.
- Tooling — npm, Vite, ESLint, Prettier, Git. Learn what a bundler actually does.
- React — components, state, effects, hooks, lists and forms. Then routing and a data-fetching library.
- Project 1 — a complete CRUD app with auth, routing, forms with validation, and proper loading/error states.
- Quality — accessibility audit, Lighthouse pass, and tests for the critical paths.
- Project 2 — something with real complexity: optimistic updates, infinite scroll, a virtualised table, or realtime via WebSocket.
- Level up — TypeScript everywhere, then SSR with Next.js, then one deep area (animation, data viz, or design systems).