Frontend
JavaScript core, the browser, React, and CSS.
Q. Explain var vs let vs const. easy ›
var is function-scoped and hoisted to the top of its function (initialized as undefined). It can be re-declared and reassigned.
let is block-scoped ({}). It is hoisted but not initialized, so accessing it before declaration throws a ReferenceError (the temporal dead zone). It can be reassigned but not re-declared in the same scope.
const is also block-scoped and must be initialized at declaration. It cannot be reassigned, but if the value is an object or array, the contents can still be mutated.
var a = 1;
var a = 2; // OK — re-declaration allowed
let b = 1;
b = 2; // OK — reassignment allowed
// let b = 3; // SyntaxError — no re-declaration
const c = { x: 1 };
c.x = 2; // OK — mutation allowed
// c = {}; // TypeError — reassignment blocked
Rule of thumb: default to
const, useletwhen you need reassignment, avoidvar.
Q. What is a closure? medium ›
A closure is a function that remembers the variables from the scope where it was created, even after that outer function has returned. It’s the basis for data privacy and hooks like useState.
function counter() {
let n = 0;
return () => ++n; // closes over n
}
const next = counter();
next(); // 1
next(); // 2 Q. What is hoisting? easy ›
Hoisting is JavaScript’s behavior of moving declarations to the top of their scope during the compile phase, before any code executes.
vardeclarations are hoisted and initialized asundefined, so you can reference them before the declaration line without an error.let/constdeclarations are hoisted but not initialized. Accessing them before their declaration throws aReferenceError— the region before initialization is called the temporal dead zone (TDZ).- Function declarations are fully hoisted (both name and body), so they can be called before they appear in the code.
- Function expressions and arrow functions assigned to variables follow the rules of the variable keyword (
var,let, orconst).
console.log(x); // undefined (var is hoisted)
var x = 5;
console.log(y); // ReferenceError (TDZ)
let y = 10;
greet(); // "Hello!" (function declaration is fully hoisted)
function greet() {
console.log("Hello!");
} Q. What does the this keyword refer to? medium ›
The value of this depends on how a function is called, not where it is defined.
| Call style | this value |
|---|---|
Method call (obj.fn()) | The calling object (obj) |
Plain call (fn()) | undefined in strict mode, globalThis in sloppy mode |
new call (new Fn()) | The newly created instance |
call / apply / bind | Whatever you explicitly pass |
| Arrow function | Inherited from the enclosing lexical scope (not its own this) |
const user = {
name: "Alice",
greet() {
console.log(this.name); // "Alice" — method call
},
greetLater() {
setTimeout(() => {
console.log(this.name); // "Alice" — arrow inherits this
}, 100);
},
};
function standalone() {
console.log(this); // undefined (strict) or globalThis (sloppy)
}
Tip: Arrow functions are ideal for callbacks inside methods because they preserve the outer
this.
Q. Explain == vs ===. easy ›
== (loose equality) compares two values after type coercion. JavaScript converts one or both operands to a common type before comparing, which can produce surprising results.
=== (strict equality) compares both value and type with no coercion. If the types differ, the result is immediately false.
0 == ""; // true — both coerced to 0
0 === ""; // false — number vs string
null == undefined; // true — special coercion rule
null === undefined; // false — different types
"1" == 1; // true — string coerced to number
"1" === 1; // false — string vs number
Best practice: Always use
===(and!==) to avoid accidental coercion bugs. The only common exception isvalue == null, which conveniently checks for bothnullandundefined.
Q. What is the difference between shallow and deep copy? medium ›
A shallow copy duplicates only the top-level properties. Nested objects and arrays are still shared between the original and the copy, so mutating a nested value affects both.
A deep copy recursively duplicates everything, so no references are shared.
const original = { name: "Alice", address: { city: "NYC" } };
// Shallow copy — nested object is shared
const shallow = { ...original };
shallow.address.city = "LA";
console.log(original.address.city); // "LA" — both changed!
// Deep copy — fully independent
const deep = structuredClone(original);
deep.address.city = "Chicago";
console.log(original.address.city); // "LA" — original untouched
Common ways to copy:
| Method | Depth |
|---|---|
Spread ({ ...obj }) / Object.assign | Shallow |
Array.from() / [...arr] | Shallow |
structuredClone(obj) | Deep |
JSON.parse(JSON.stringify(obj)) | Deep (but drops functions, undefined, Date objects, etc.) |
Tip:
structuredClone(available in all modern runtimes) is the safest built-in deep copy method.
Q. What are map, filter, and reduce? easy ›
These three array methods are the foundation of functional-style data transformation in JavaScript. None of them mutate the original array.
map — transforms every element and returns a new array of the same length.
[1, 2, 3].map(n => n * 2); // [2, 4, 6]
filter — returns a new array containing only elements that pass the test.
[1, 2, 3, 4].filter(n => n % 2 === 0); // [2, 4]
reduce — boils the array down to a single value by running an accumulator function on each element.
[1, 2, 3, 4].reduce((sum, n) => sum + n, 0); // 10
Chaining them together:
const orders = [
{ item: "Book", price: 12 },
{ item: "Pen", price: 2 },
{ item: "Bag", price: 35 },
];
const total = orders
.filter(o => o.price > 5) // keep expensive items
.map(o => o.price) // extract prices
.reduce((sum, p) => sum + p, 0); // sum them
// total = 47 Q. What is the Virtual DOM and how does React use it? medium ›
The Virtual DOM is a lightweight in-memory copy of the UI. On a state change, React builds a new tree, diffs it against the old one (reconciliation), and updates only the changed real-DOM nodes — far cheaper than re-rendering everything.
Q. Explain useState and useEffect. medium ›
useState adds local state and returns the value plus a setter. useEffect runs side effects (data fetching, subscriptions) after render; its dependency array controls when it re-runs, and the returned function cleans up.
useEffect(() => {
const id = setInterval(tick, 1000);
return () => clearInterval(id); // cleanup
}, []); // [] = run once on mount Q. Explain debounce vs throttle. hard ›
Both techniques limit how often a function fires, but they work differently.
Debounce — waits until the user stops triggering the event for a set delay, then fires once. Each new trigger resets the timer.
Use case: search-as-you-type input, window resize handler.
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
input.addEventListener("input", debounce(handleSearch, 300));
Throttle — fires at most once per interval, no matter how many times the event triggers. It guarantees a steady execution rate.
Use case: scroll listener, drag handler, rate-limited API calls.
function throttle(fn, interval) {
let lastTime = 0;
return (...args) => {
const now = Date.now();
if (now - lastTime >= interval) {
lastTime = now;
fn(...args);
}
};
}
window.addEventListener("scroll", throttle(handleScroll, 200));
In short: Debounce collapses a burst into one call at the end. Throttle spreads calls evenly across time.
Q. localStorage vs sessionStorage vs cookies. easy ›
All three store data on the client, but they differ in lifetime, size, and behavior.
| Feature | localStorage | sessionStorage | Cookies |
|---|---|---|---|
| Lifetime | Until explicitly cleared | Until the tab/window closes | Set by Expires / Max-Age (or session) |
| Size limit | ~5-10 MB | ~5-10 MB | ~4 KB per cookie |
| Sent to server | No | No | Yes, on every matching HTTP request |
| Scope | Same origin, all tabs | Same origin, single tab | Same origin (configurable with Domain, Path) |
| API | setItem / getItem | setItem / getItem | document.cookie string or Set-Cookie header |
// localStorage
localStorage.setItem("theme", "dark");
localStorage.getItem("theme"); // "dark"
// sessionStorage
sessionStorage.setItem("token", "abc123");
// Cookie
document.cookie = "lang=en; max-age=86400; path=/";
When to use what: Use cookies when the server needs the data (auth tokens with
HttpOnly). UselocalStoragefor persistent user preferences. UsesessionStoragefor temporary, tab-specific data.
Q. What is event bubbling and delegation? medium ›
Event bubbling is the DOM’s default propagation model: when an event fires on an element, it first runs handlers on that element, then bubbles up through every ancestor to the document root.
click on <button>
→ button handler
→ div handler
→ body handler
→ document handler
You can stop propagation with event.stopPropagation(), but this is rarely needed.
Event delegation leverages bubbling by attaching a single listener on a parent instead of one listener on every child. The parent checks event.target to determine which child was actually clicked.
// Instead of adding a listener to every <li>...
document.querySelector("ul").addEventListener("click", (e) => {
if (e.target.tagName === "LI") {
console.log("Clicked:", e.target.textContent);
}
});
Why delegation is useful:
- Performance — one listener instead of hundreds.
- Dynamic elements — new children added later are automatically handled.
- Cleaner code — less setup and teardown logic.
Q. What happens when you type a URL and press Enter? hard ›
This classic question covers the full lifecycle of a web request:
-
URL parsing — the browser parses the URL into protocol, host, port, and path.
-
DNS resolution — the browser checks its cache, then the OS cache, then queries DNS servers to resolve the domain name to an IP address.
-
TCP connection — a TCP three-way handshake (SYN, SYN-ACK, ACK) establishes a connection with the server.
-
TLS handshake (HTTPS) — client and server negotiate encryption, exchange certificates, and establish a secure session.
-
HTTP request — the browser sends the HTTP request (method, headers, cookies) to the server.
-
Server processing — the server routes the request, runs application logic, queries databases if needed, and builds a response.
-
HTTP response — the server sends back a status code, headers, and the response body (HTML).
-
HTML parsing — the browser parses HTML into the DOM tree. When it encounters CSS, it builds the CSSOM tree. Scripts may block parsing unless marked
asyncordefer. -
Render tree — the DOM and CSSOM are combined into a render tree (only visible elements).
-
Layout — the browser calculates the exact position and size of every element.
-
Paint — pixels are drawn to the screen, layer by layer.
-
Compositing — layers are combined in the correct order and sent to the GPU for display.
This is a great question to show breadth of knowledge. Interviewers don’t expect every detail — focus on the areas you know best.
Q. What are Core Web Vitals? medium ›
Core Web Vitals are a set of real-world performance metrics defined by Google that measure user experience on the web. They factor into search ranking.
| Metric | Measures | Good threshold |
|---|---|---|
| LCP (Largest Contentful Paint) | Loading — how quickly the main content appears | < 2.5 s |
| INP (Interaction to Next Paint) | Responsiveness — delay between user input and visual update | < 200 ms |
| CLS (Cumulative Layout Shift) | Visual stability — how much the page layout shifts unexpectedly | < 0.1 |
How to improve each:
- LCP — optimize images (modern formats,
loading="lazy"), preload critical resources, use a CDN, reduce server response time. - INP — break up long tasks, use
requestIdleCallback, minimize main-thread work, defer non-essential JavaScript. - CLS — set explicit
width/heighton images and embeds, avoid injecting content above existing content, usefont-display: swap.
Tools to measure: Chrome DevTools (Lighthouse), PageSpeed Insights,
web-vitalsnpm package, and Chrome UX Report (CrUX) for field data.
Q. Props vs state. easy ›
Props (short for properties) are values passed from a parent component to a child. They are read-only — a child should never modify its own props.
State is data managed internally by a component. It can be updated with a setter (e.g., setState or the useState hook), and changes trigger a re-render.
| Props | State | |
|---|---|---|
| Owner | Parent component | The component itself |
| Mutable? | No (read-only for the child) | Yes (via setter function) |
| Triggers re-render? | Yes, when the parent passes new values | Yes, when updated |
| Purpose | Configure a child from the outside | Track internal, changing data |
function Greeting({ name }) { // name is a prop
const [count, setCount] = useState(0); // count is state
return (
<div>
<p>Hello, {name}!</p>
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
</div>
);
}
Key idea: Data flows down through props. When a child needs to communicate up, the parent passes a callback function as a prop.
Q. What are keys in lists and why are they important? easy ›
Keys give React a stable identity for each item in a list. During reconciliation (diffing), React uses keys to figure out which items were added, removed, or reordered — without keys, React falls back to comparing by index, which can cause bugs and wasted renders.
// Good — unique, stable id
{users.map(user => (
<UserCard key={user.id} name={user.name} />
))}
// Bad — array index as key
{users.map((user, index) => (
<UserCard key={index} name={user.name} />
))}
Why array index is problematic:
- If items are reordered, inserted, or deleted, the index shifts and React may reuse the wrong component instance, leading to stale state or broken animations.
Rules for good keys:
- Use a unique, stable identifier from your data (database ID, slug, etc.).
- Keys must be unique among siblings (not globally).
- Never generate keys on the fly (e.g.,
Math.random()) — this defeats the purpose.
Tip: If your data has no natural ID, consider adding one at the data layer rather than relying on index.
Q. useMemo vs useCallback — when do you use them? hard ›
Both hooks memoize values between renders to avoid unnecessary work, but they serve different purposes.
useMemo — memoizes a computed value. The factory function only re-runs when its dependencies change.
const sorted = useMemo(
() => items.sort((a, b) => a.price - b.price),
[items]
);
useCallback — memoizes a function reference. Useful when passing callbacks to child components wrapped in React.memo.
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []); // stable reference across renders
useCallback(fn, deps) is essentially shorthand for useMemo(() => fn, deps).
When to use them:
- An expensive calculation runs on every render (use
useMemo). - A child component wrapped in
React.memoreceives a callback prop and re-renders unnecessarily (useuseCallback). - A value is used as a dependency in another hook and needs referential stability.
When NOT to use them:
- The computation is cheap — memoization itself has overhead (memory + comparison).
- The component rarely re-renders anyway.
- You’re optimizing prematurely without measuring.
Rule of thumb: Write code without memoization first. Add
useMemo/useCallbackonly when profiling reveals a real performance issue.
Q. What is the rules-of-hooks constraint? medium ›
React hooks rely on a stable call order between renders. To guarantee this, React enforces two rules:
1. Only call hooks at the top level
Never call hooks inside loops, conditions, or nested functions. Every render must invoke the same hooks in the same order.
// Bad — conditional hook
if (loggedIn) {
useEffect(() => { /* ... */ }); // breaks call order
}
// Good — condition inside the hook
useEffect(() => {
if (loggedIn) { /* ... */ }
}, [loggedIn]);
2. Only call hooks from React functions
Hooks can only be called from:
- Function components
- Custom hooks (functions whose name starts with
use)
They cannot be called from regular JavaScript functions, class components, or event handlers.
Why these rules exist:
React tracks hooks by their call index (first hook = index 0, second = index 1, etc.). If a hook is conditionally skipped, every subsequent hook shifts position and maps to the wrong state — causing subtle and hard-to-debug errors.
Tip: The
eslint-plugin-react-hookspackage (included in Create React App and Next.js) catches violations automatically.
Q. How do you manage global state in React? medium ›
There is no single “right” answer — the best approach depends on the type and frequency of the shared data.
Context API (built-in)
Good for low-frequency, rarely-changing values like theme, locale, or auth status. Every consumer re-renders when the context value changes, so it is not ideal for rapidly updating state.
const ThemeCtx = createContext("light");
function App() {
return (
<ThemeCtx.Provider value="dark">
<Page />
</ThemeCtx.Provider>
);
}
Zustand / Redux / Jotai (external libraries)
Better for larger, frequently-changing state. These libraries provide selectors so components only re-render when the specific slice of state they use changes.
// Zustand example
const useStore = create((set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 })),
}));
React Query / TanStack Query (server state)
Best for data fetched from APIs. Handles caching, background refetching, stale-while-revalidate, and error/loading states out of the box — keeping server state out of your global store.
Guideline: Start with local state (
useState) and lift only when needed. Reach for Context for simple global values, a dedicated store for complex client state, and a data-fetching library for server state.
Q. What causes unnecessary re-renders and how do you prevent them? hard ›
A React component re-renders when:
- Its state changes.
- Its props change (by reference).
- Its parent re-renders (even if the child’s props are the same).
- A context it consumes changes.
Most re-renders are fast and harmless, but they become a problem in large lists or computation-heavy components.
Prevention strategies:
React.memo — wraps a component so it only re-renders when its props change (shallow comparison).
const ExpensiveList = React.memo(({ items }) => {
return items.map(item => <Row key={item.id} {...item} />);
});
useCallback / useMemo — stabilize callback and object references passed as props so React.memo comparisons succeed.
const handleClick = useCallback(() => { /* ... */ }, []);
const config = useMemo(() => ({ theme: "dark" }), []);
Component splitting — move state as close to where it’s used as possible. Only the component holding the state re-renders.
// Before: entire page re-renders on every keystroke
// After: only SearchInput re-renders
function SearchInput() {
const [query, setQuery] = useState("");
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}
Store selectors — with Zustand or Redux, subscribe to specific slices so unrelated state changes don’t trigger a re-render.
Key principle: Profile first (React DevTools Profiler), optimize second. Premature memoization adds complexity without measurable benefit.
Q. CSR vs SSR vs SSG (and where does Next.js fit)? hard ›
These are three strategies for turning your components into HTML that the browser can display.
CSR (Client-Side Rendering)
The server sends a nearly empty HTML shell and a JavaScript bundle. The browser downloads, parses, and executes the JS to render the page. Good for highly interactive apps (dashboards), but the initial load can be slow and SEO is limited.
SSR (Server-Side Rendering)
The server renders the full HTML on every request and sends it to the browser. The page is visible immediately, then JS “hydrates” it to make it interactive. Great for SEO and dynamic, personalized pages, but every request hits the server.
SSG (Static Site Generation)
HTML is generated at build time and served from a CDN. Fastest possible load since there is no per-request work. Ideal for content that does not change often (blogs, docs, marketing pages).
| CSR | SSR | SSG | |
|---|---|---|---|
| Rendered | In the browser | On the server per request | At build time |
| Time to first content | Slow (JS must load) | Fast | Fastest |
| SEO | Poor (without extra work) | Good | Good |
| Dynamic data | Yes | Yes | Limited (rebuild needed) |
Where Next.js fits:
Next.js lets you choose the rendering strategy per route. A single app can mix SSG pages, SSR pages, and fully client-rendered components. It also supports ISR (Incremental Static Regeneration) — static pages that revalidate in the background after a set interval, combining the speed of SSG with fresher data.
Q. Flexbox vs Grid. easy ›
Both are CSS layout systems, but they solve different problems.
Flexbox is one-dimensional — it lays out items along a single axis (row or column). It excels at distributing space and aligning items within a line.
.nav {
display: flex;
justify-content: space-between;
align-items: center;
}
Grid is two-dimensional — it controls both rows and columns simultaneously. It is ideal for page-level layouts and any design that needs alignment in both directions.
.dashboard {
display: grid;
grid-template-columns: 250px 1fr;
grid-template-rows: auto 1fr auto;
gap: 1rem;
}
| Flexbox | Grid | |
|---|---|---|
| Axes | One (row or column) | Two (rows and columns) |
| Best for | Navbars, toolbars, card rows, centering | Page layouts, dashboards, galleries |
| Content vs layout | Content-driven (items size themselves) | Layout-driven (you define the grid, items fill it) |
In practice: You often use both. Grid for the overall page structure and Flexbox for component-level alignment within each grid cell.
Q. Explain the box model and box-sizing. easy ›
Every HTML element is a rectangular box made up of four layers (from inside out):
- Content — the actual text, image, or child elements.
- Padding — transparent space between the content and the border.
- Border — the visible edge around the padding.
- Margin — transparent space outside the border, separating the element from its neighbors.
box-sizing property:
By default (content-box), width and height apply only to the content area. Padding and border are added on top, making the element larger than the declared size.
With border-box, width and height include content + padding + border. This makes sizing far more predictable.
/* Apply border-box globally — recommended */
*, *::before, *::after {
box-sizing: border-box;
}
.card {
width: 300px;
padding: 20px;
border: 2px solid #ccc;
/* With border-box: total width is still 300px */
/* With content-box: total width would be 300 + 40 + 4 = 344px */
}
Best practice: Set
box-sizing: border-boxon all elements at the start of your stylesheet. Nearly every modern CSS reset or framework does this.
Q. How does CSS specificity work? medium ›
When multiple CSS rules target the same element, the browser uses specificity to decide which rule wins. Specificity is calculated as a three-part score: (A, B, C).
| Level | What counts | Example |
|---|---|---|
| A — IDs | #header | (1, 0, 0) |
| B — Classes, attributes, pseudo-classes | .nav, [type="text"], :hover | (0, 1, 0) |
| C — Elements, pseudo-elements | div, ::before | (0, 0, 1) |
Comparing specificity: A beats B beats C, regardless of count.
/* (0, 1, 0) — one class */
.button { color: blue; }
/* (1, 0, 0) — one ID (wins) */
#submit { color: red; }
/* (0, 0, 2) — two elements (loses to one class) */
div button { color: green; }
Tie-breaker: When two selectors have equal specificity, the later rule in the stylesheet wins.
Special cases:
- Inline styles (
style="...") override any selector-based rule. !importantoverrides everything, including inline styles. Use it sparingly.- The universal selector (
*), combinators (>,+,~), and:where()add zero specificity. :is()and:not()take the specificity of their most specific argument.
Tip: Keep specificity low and flat. Prefer classes over IDs, avoid
!important, and use a consistent naming convention (like BEM) to prevent specificity wars.