The Machine Coding Round
An interview playbook for the 2-hour frontend build — how to clarify, plan, and structure components under time pressure without freezing on the prompt.
A repeatable playbook for the frontend machine-coding interview — how to clarify, justify, plan, and build under a timer so you don't freeze when the prompt is "build YouTube in two hours."
The machine-coding round is not a typing race. It's judged on how you think out loud, scope the problem, justify choices, and structure code. Spend the first ~10 minutes talking, not coding — that's what separates a senior signal from a junior one.#
The timebox#
A blank editor and "build YouTube" is paralysing only if you have no process. Run the same four phases every time, with a timer. The talking phases are short but decisive — they set the interviewer's impression before you write a line.
Requirements clarification — agree on the feature scope.
Tech stack — name your choices and why.
Planning — HLD layout + LLD component tree.
Build — skeleton first, then feature by feature, narrating.
- Don't code in the first 10–15 minutes: clarifying and planning feel slow but save you from building the wrong scope. Most candidates lose points by jumping straight to code.
- Scope down, ruthlessly: you can't build all of YouTube in two hours. Pick a handful of features and say so out loud — demonstrating judgment beats half-finishing everything.
- Keep a timer visible: practice the whole thing under a two-hour clock so the pacing is muscle memory, not a surprise.
Treat it like a road trip, not a drag race. Five minutes with the map (clarify + plan) gets you there faster than flooring it in a random direction. The interviewer is grading your navigation, not your top speed.
The prompt is intentionally huge. Your first move is to narrow it to an agreed feature list with the interviewer. A vague app can be built a thousand ways — pinning the scope means you and the interviewer are building the same app.#
Walk the reference app, then cut scope#
Open the real product (e.g. youtube.com) and narrate what it has — header, sidebar, filter buttons, video grid, the watch page, search, comments. Then propose which slice you'll actually build in the time, and confirm it.
- Name the parts you see: "There's a header with a hamburger, logo, search, and user icon; a collapsible sidebar; a button row; a video grid; and a watch page." This shows you've decomposed the UI.
- Flag complexity honestly: call out that comments or search are features complex enough to be their own task — then ask whether they're in scope. Naming the hard parts is a senior move.
- Get explicit agreement: "So I'll build the home feed, the collapsible sidebar, and the watch page with an embedded video — does that work?" Now you can't be marked down for skipping something you agreed to skip.
Don't assume the scope
Building features the interviewer didn't want wastes your limited time. Ask; don't guess.
Why even discuss what you both can see?#
The interviewer has used YouTube too — so why describe it? Because there are a thousand ways to build it, and the discussion aligns your approach with theirs. It's about shared understanding, not information.
- Alignment, not exposition: the value is agreeing on your plan, surfacing assumptions early so there are no surprises at minute 90.
- It signals seniority: a junior starts typing; a senior scopes first. The conversation itself is part of what's being graded.
Listing your stack is table stakes; justifying it is the signal. For every choice — framework, styling, state, routing, bundler, testing — have a one-line reason ready. Naming a tool without a reason looks memorized; a reason looks like judgment.#
Two layers: UI and data#
Frame the stack around the two layers every frontend has: the UI layer (how it looks and renders) and the data layer (how state and server data flow). Decide a tool for each and say why.
| Concern | Pick | One-line justification |
|---|---|---|
| Framework | React | Component model, huge ecosystem, the role's stack. |
| Styling (UI) | Tailwind | Utility-first → build UI fast with no context-switching to CSS files. |
| State (data) | Redux | Predictable central store for a large, data-heavy app like YouTube. |
| Routing | React Router | Client-side routing for the home ↔ /watch pages. |
| Bundler | Webpack / Vite | CRA ships Webpack; Vite if starting fresh — either bundles & serves. |
| Testing | Jest + RTL | Jest runs the tests; React Testing Library queries the rendered UI. |
| Forms (if any) | Formik | Handles values, validation, errors for big forms without boilerplate. |
- Context vs Redux is a judgment call: for a small app (counter, to-do) say
Context— no complex data flow. For a large app (YouTube) sayRedux— predictable central store. Naming the threshold is the point. - Mention things others forget: bundler, testing library, and storage rarely come up — raising them unprompted reads as thinking about scalability, not just shipping.
- Have the "why Tailwind / why Redux" answer ready: "modern, fast to develop, strong tooling." A confident one-liner per tool is enough.
When the interviewer constrains you#
Usually interviewers accept your picks — they're rarely rigid. Occasionally one will impose a constraint ("use plain CSS, not Tailwind") to see you adapt. Take it gracefully and move on.
- Don't argue a hard constraint: if they say plain CSS, use plain CSS. Flexibility is also a signal.
- On scaffolding, be pragmatic: say you'd normally configure a bundler from scratch for a
big app, but to respect the time you'll start with
create-react-app(or Vite) and explain you could set up Webpack/Babel/Parcel by hand if needed.
Cap this phase at 5 minutes
The stack chat is high-signal but short. Don't let it eat your build time — state your picks, give reasons, move to planning.
Two levels of planning: HLD (what goes where on screen, which pages exist) and LLD (the component tree). Decide the components before you type, and the build becomes filling in a skeleton you already drew.#
High-level design: the screens#
Sketch the home page (header, sidebar, button row, video grid) and the watch page (same
header, the video, suggestions, comments). Note the routes: / for home and
/watch for a video.
- Reuse across pages: point out the header (and other shared chrome) appears on both home and watch — that's one component, not two. Spotting reuse early is a design signal.
- Map clicks to navigation: "Clicking a video card routes to
/watch?v=ID, which reads the id from the URL and embeds the player." Stating the data flow shows you've thought past layout.
Low-level design: the component tree#
Break the UI into named components and their nesting. This is the artifact you'll literally build from — each box becomes a file.
// the component tree you'll build App ├ Head // hamburger, logo, search, user icon └ Body ├ Sidebar // menu items (collapsible) └ MainContainer ├ ButtonList // filter chips └ VideoContainer └ VideoCard // one per video WatchPage // route /watch ├ Head // reused └ WatchVideo // embedded player + (suggestions, comments)
- Name components deliberately: Head, Body, Sidebar, MainContainer, ButtonList, VideoContainer, VideoCard. Clear names make the tree self-documenting and the build mechanical.
- Go only as deep as you need now: sketch the skeleton; you can decompose further (e.g. inside VideoCard) when you reach it. Don't over-plan.
- This is the payoff of planning: once the tree exists, coding is "create the file, drop it in its parent." No mid-build architecture decisions.
Build the skeleton end-to-end first (empty components wired together), then fill each one. Use a scaffold so you don't waste time on tooling, and lean on editor shortcuts — in an interview, speed of execution is fair game (unlike practice, where doing it by hand teaches more).#
Scaffold, don't hand-configure#
Setting up Webpack/Babel/Parcel from zero burns time you don't have. Start from a scaffold and say so: "I'll use create-react-app for speed; for a large production app I'd configure my own bundler." That shows you know the tradeoff.
// scaffold + run (know what each word means) npx create-react-app namaste-youtube // npx = execute a package once; create-react-app = the generator npm run start // check the script in package.json; CRA serves on :3000
- Know why you type the command:
npxruns a package once without installing it globally;create-react-appis the generator package. Explaining this casually reads as depth. - CRA gives you Webpack + Jest + RTL for free: mention that the scaffold has already wired the bundler and testing you'd otherwise set up by hand.
- Find the run command in
package.json: checkscripts— it'snpm run startfor CRA. Don't guess.
Wire Tailwind, then build the skeleton#
Add Tailwind (install, init config, set the content glob, add
the three directives to your CSS), confirm a test class renders, then create every component as an
empty shell and nest them per your tree.
// Tailwind in a CRA project npm i -D tailwindcss npx tailwindcss init // tailwind.config.js content: ["./src/**/*.{js,jsx}"], // files Tailwind scans // index.css @tailwind base; @tailwind components; @tailwind utilities;
- Skeleton before detail: create Head, Body, Sidebar, MainContainer, ButtonList, VideoContainer (empty), nest them, and confirm the page renders. Then flesh out one component at a time.
- Two classic Tailwind traps: a wrong
contentglob silently purges your classes; and forgetting to import the CSS file means no styles apply at all. Verify with a quicktext-3xl font-bold. - Layout tools:
flexfor simple rows;grid grid-cols-12+col-span-*for a header split into logo / search / user. Knowing both reads as fluency.
Shortcuts & live data#
rafce snippet
Type rafce in a .jsx file to scaffold an
arrow-function component with export instantly. Fair to use under interview time pressure.
Read the URL param
On /watch?v=ID, use useSearchParams().get("v") to get the video id, then embed the player
or fetch its data.
Use a live API
Wire a real data source (e.g. YouTube's API) for the feed, search, and comments — live data makes the build feel production-grade.
Keep the console clean
Treat warnings/errors as bugs. JSX uses className and camelCase DOM
props — fix "invalid DOM property" warnings rather than ignoring them.
- Practice the hard way, perform the fast way: when learning, configure tooling by hand so you understand it; in the interview, use scaffolds and snippets to move quickly. Knowing the manual path lets you explain the shortcut.
- Don't over-polish CSS: "decent and functional" beats pixel-perfect-but-unfinished. Get features working, then refine if time allows.
Doing three things at once — coding, explaining, and thinking ahead — is a learnable skill, and it's the one that passes interviews. A silent correct solution scores lower than a narrated one, because the interviewer is grading your reasoning, not just your output.#
Narrate the decisions, not the keystrokes#
As you build, say what you're doing and why: "I'm making Head its own component so the watch page can reuse it." That running commentary is exactly the signal interviewers reward.
- Explain choices, not syntax: "I'll keep this in Redux so the header and feed share it" is useful; reading the code aloud isn't. Talk at the level of decisions.
- Think one step ahead out loud: "Next I'll wire the route so a card click opens
/watch." It shows direction and keeps the interviewer with you. - Silence reads as uncertainty: even when stuck, narrate your debugging ("the class isn't applying — let me check the content glob"). Visible problem-solving is a plus.
How to practice it#
This is rehearsable. Build the whole app yourself under a two-hour timer, recording your screen and voice, explaining every step as if to an interviewer. Do the 5-min clarify and 5-min plan out loud too.
- Record & rewatch: recording forces you to speak while coding and exposes where you go quiet or fumble. It's the fastest way to build the habit.
- Run the real clock: a visible two-hour timer trains your pacing — clarify, plan, build — so the real round feels familiar.
- Practice removes the need to Google: with enough reps you won't need to look things up mid-round; the flow becomes automatic.
It's like a cooking show. The chef isn't just making the dish — they're explaining each step while doing it and glancing at what's next. That commentary is the performance; the finished plate alone wouldn't earn the show. Your narration is the show.
Machine-coding rounds reuse a small set of "famous" sub-problems. Three show up constantly: a higher-order component, a performant search bar, and recursive nested comments. Drill these until they're automatic — each one is also a clean excuse to show off data-structure and performance instincts, which is where seniority shows. #
Higher-order components#
A higher-order component (HOC) is just a function that takes a component and returns a new component — usually the same one with a small modification wrapped around it. Nothing more.
// takes VideoCard, returns a bordered version of it const withAdLabel = (VideoCard) => { return (props) => ( <div className="p-5 m-2 border border-red-900"> <label>Ad</label> <VideoCard {...props} /> // pass props straight through </div> ); }; // usage: render <AdVideoCard info={...} /> const AdVideoCard = withAdLabel(VideoCard);
- Real-world framing wins: a YouTube ad card is a normal video card with a small change (an "Ad" label, no channel name). That's exactly the "modify an existing component a little" use-case — a great example to offer the interviewer.
- Forward the props: the wrapped component still needs its data, so spread
{...props}into it. Forgetting this is the classic "cannot destructure property of undefined" bug. - Guard against empty first render: if data loads async, the wrapper may render before
videos[0]exists — gate on it (videos[0] && ...) so the first paint doesn't crash.
An HOC is a gift-wrapping service. You hand it any product (component); it hands back the same product in fancier packaging (a border, a label, extra behavior). The thing inside is unchanged — only the wrapping is added.
Debounced search — don't call the API on every keystroke#
Type-ahead search is the most-asked feature ("build a search bar"). The naive version fires an API call per keypress — 7 letters, 7 calls. Debouncing waits for a pause in typing before calling, collapsing 7 calls into 2–3.
// in Head: state bound to the input const [query, setQuery] = useState(""); useEffect(() => { const timer = setTimeout(() => getSuggestions(query), 200); return () => clearTimeout(timer); // cleanup cancels the pending call }, [query]); const getSuggestions = async () => { const data = await fetch(YOUTUBE_SEARCH_API + query); const json = await data.json(); setSuggestions(json[1]); // suggestions live at index 1 };
- Why it works — the cleanup is the trick: every keystroke re-runs the effect, which
first runs the previous effect's cleanup (
clearTimeout) and then starts a fresh timer. If the next key lands within 200ms, the old timer is cancelled before it fires. Only a real pause lets a timer survive to call the API. - It's a real scale issue, not a micro-optimization: "iPhone Pro Max" is ~14 keystrokes — 14 calls per user. At a thousand users that's 14,000 vs. ~3,000 calls. On Flipkart/Google scale the difference is enormous.
- Tune the delay to the product: ~200ms (Flipkart) feels deliberate; a smaller delay (YouTube) feels instant but costs more calls. Mention the tradeoff — better UX vs. fewer requests.
- Doing this by hand in plain JS is fiddly; in React,
useEffect+ its cleanup gives you debouncing almost for free. Say that — it shows you understand the lifecycle, not just the snippet.
Debouncing is an elevator door. Each new person stepping in resets the close timer; the doors only shut once there's a gap with no one entering. Fast typing keeps resetting the timer; a pause lets it finally "close" and fire the call.
Cache the results — structure beats brute force#
The next level: don't re-fetch a query you've already searched. Cache results in the store, keyed by the query string, and read the cache before fetching. The data structure you choose is the whole point.
// search slice: cache is an OBJECT keyed by query, not an array const searchSlice = createSlice({ name: "search", initialState: {}, reducers: { cacheResults: (state, action) => { state = Object.assign(state, action.payload); // merge {query: results} }, }, }); // in Head, before fetching: if (searchCache[query]) setSuggestions(searchCache[query]); // O(1) hit else { /* fetch, then */ dispatch(cacheResults({ [query]: json[1] })); }
- Object/Map, not array: looking up a query in an array is O(n) (
includes/indexOfscan one by one). Keying an object (a hash map) by the query string makes lookup O(1). This is the data-structures answer interviewers fish for — "DS matters even on the frontend." - This beats Flipkart, matches YouTube: with caching, re-typing a searched term makes zero calls. Flipkart re-fetches on revisit; YouTube caches — aim for the YouTube behavior.
- Bound the cache (bonus points): note you'd cap it (e.g. 100 keys) and evict with an LRU / FIFO policy so a long session can't bloat the store. You don't have to build it — naming it signals depth.
N-level nested comments — recursion in components#
"Build a comment section that nests to any depth" (think Reddit) is a favourite because it needs UI and data-structure and recursion. The trick is modelling the data as a tree, then rendering a component that calls itself.
// each comment can contain replies that are themselves comments → a tree const data = [{ name, text, replies: [ {name, text, replies:[...] } ] }]; const CommentsList = ({ comments }) => comments.map((c, i) => ( <Comment key={i} data={c} /> )); const Comment = ({ data }) => { const { name, text, replies } = data; return ( <> <div>{name}: {text}</div> <div className="pl-5 border-l ml-5"> <CommentsList comments={replies} /> // ← recursion </div> </> ); };
- Model the data first: a comment is
{ name, text, replies: [] }andrepliesis an array of the same shape. Get the recursive data structure right and the UI falls out of it. - The component recurses on itself:
CommentsListrenders eachComment, and eachCommentrenders aCommentsListfor its replies. That mutual call is what supports unlimited depth without breaking. - Indent with a left border:
pl-5 border-l ml-5on the replies wrapper gives the Reddit-style nesting line for free. - Keys, honestly: index keys work for a static demo, but say out loud you'd use a real comment id in production — index-as-key is a known anti-pattern when the list mutates.
Nested comments are Russian nesting dolls. Each doll (comment) can contain a smaller doll of the exact same kind (a reply), all the way down. One "open this doll" routine (the component) handles every level — it just calls itself on whatever's inside.
"Build YouTube live chat" (or a stock ticker, or WhatsApp) is really two problems: how to get data that keeps changing, and how to render it without freezing the page. Nail the data layer (WebSockets vs API polling) and the UI layer (cap the rendered list), and you've answered a question that shows up in nearly every frontend system-design round.#
Two transports — and how to choose#
For data that updates continuously there are two real options. The choice is driven by one question: how real-time does it need to be?
| Transport | Shape | Use when |
|---|---|---|
| WebSocket | Bi-directional, persistent — one handshake, then either side pushes anytime, no fixed interval. | Near real-time & order matters: chat (WhatsApp), trading / stock tickers, multiplayer. |
| API polling | One-directional — the UI requests on a fixed interval; data flows server→client. | "Eventually fresh" is fine: Gmail inbox, cricket commentary, YouTube live chat & comments. |
- Map each app to a transport out loud: Gmail → polling (a 10s-old email is fine); a trading app → WebSocket (milliseconds matter); WhatsApp → WebSocket (out-of-order messages would be a disaster). Saying why is the signal.
- Tune the poll interval to the domain: a cricket score app can poll every ~25s (no ball comes faster); YouTube live chat polls ~every 1.5s to feel real-time. The interval is a UX-vs-cost lever, not a constant.
- WebSockets aren't free: a persistent connection is comparatively heavy. Don't reach for one when polling is good enough — that judgment reads as senior.
- The surprising truth — YouTube polls comments: open the network tab on a live
stream and you'll see thousands of
get_live_chatcalls. "API calls are expensive" is a myth at the right interval; the real cost is the UI, not the requests.
A WebSocket is a phone call — dial once, then both sides talk freely until you hang up. Polling is checking the mailbox — you walk out every N minutes to see if anything arrived. Use the call for a live conversation; the mailbox for "did anything come in?"
The data layer — poll, then push into the store#
Implement polling with setInterval inside a useEffect, fetch each tick, and dispatch the new messages into Redux. The
component subscribes to the store and re-renders itself — you never touch the DOM directly.
// LiveChat: poll on an interval, dispatch into the store const dispatch = useDispatch(); useEffect(() => { const id = setInterval(() => { // real version: fetch(LIVE_CHAT_API).then(r => r.json()) dispatch(addMessage({ name: getRandomName(), message: getRandomMsg() })); }, 1500); return () => clearInterval(id); // cleanup: stop polling on unmount }, []); // run once after mount
- Empty dependency array = run once: you want a single interval set up after mount, not a new one every render. Pair it with the cleanup.
- Always clear the interval: returning
clearInterval(id)is mandatory — without it, leaving the page keeps firing calls forever (a real memory/network leak). Same rule forsetTimeout. - Swap the fake for a real fetch: the structure is identical with live data —
fetch→.json()→dispatch(addMessage(...)). Build with stub data first to prove the loop, then wire the API. - Test the loop before the payload:
console.loginside the interval first to confirm it ticks, then add the dispatch. Step-by-step beats "write it all, then debug."
The UI layer — cap the list so the page never freezes#
This is the part most candidates miss. If you keep appending message divs forever, the DOM grows without bound and the page eventually freezes. The fix — the one YouTube actually uses — is to keep only the latest N messages and drop the oldest.
// chatSlice reducer: add newest, evict oldest past the cap addMessage: (state, action) => { state.messages.splice(LIVE_CHAT_LIMIT, 1); // drop one past the limit state.messages.unshift(action.payload); // newest goes to the front } // LIVE_CHAT_LIMIT lives in constants → tune per device/browser
splice(LIMIT, 1)+unshift: every time you add a message at the front, you remove one at the cap — so the list size stays fixed no matter how long the page is open. Leave it running ten hours and it won't crash.- Make the cap a constant, not a magic number: put
LIVE_CHAT_LIMITin your constants file so you can tune it — a beefy desktop on the latest Chrome can hold more than an old mobile. Configuring per-device is the senior flourish. - Why no timestamps on live chat: because order isn't guaranteed (polling returns batches), showing exact times would expose the illusion. Dropping the timestamp is a deliberate product call, not an oversight.
- Render direction: use
flex-col-reverse+overflow-y-scrollso newest sits at the bottom and the column scrolls like a real chat. - Keys, again: live payloads carry a real id — use it as the
key, never the array index, since the list constantly mutates.
The capped list is a conveyor belt with a fixed number of slots. Each new box that rolls on pushes the oldest box off the far end. The belt never overflows the room — it just keeps the most recent items moving, exactly like a live chat window.
You'll use useState and useEffect 99% of the time.
The performance hooks come up when an interviewer probes optimization — and the points go to
whoever can explain why a component re-renders and what that costs, not
just recite an API. The foundation: every render re-runs the component function from scratch, so
every plain variable is recreated.#
The render-cycle truth (the foundation)#
A component is just a function. When state or props change, React calls that function again — a fresh execution context, fresh memory, all local variables recreated. That single fact explains every hook below.
// a plain `let` is wiped on every re-render let x = 0; // recreated to 0 each render const [y, setY] = useState(0); // React persists this across renders // clicking "x++" mutates x — but no re-render, so the UI never updates, // and the next state-driven re-render resets x back to 0.
- Why a
letdoesn't work as UI state: incrementing it changes the value in memory, but React doesn't re-render, so the screen never updates — and the moment any other state change triggers a render, the variable is reinitialized and your value is lost. - Why
useStateworks: React stores it outside the function's per-render memory and re-renders when you call the setter. That's the entire reason state hooks exist — a question many React devs can't answer in depth. - StrictMode double-renders in dev: components mount/render twice in development (not
production) so React can surface impure render logic. If your
console.logfires twice, that's why — don't "fix" it by removing StrictMode in real code.
Each render is a fresh whiteboard. Plain variables are written on it and wiped clean every time you redraw. State and refs are written in a notebook React keeps off to the side — they survive each wipe.
useMemo — cache an expensive calculation#
If a render does heavy work (say, computing the Nth prime), that work re-runs on
every render — even one triggered by an unrelated state change like toggling a theme.
useMemo caches the result and only recomputes when its dependencies change.
// without memo: findPrime runs on EVERY render — theme toggle freezes the page const prime = findPrime(number); // with memo: cached between renders, recomputed only when `number` changes const prime = useMemo(() => findPrime(number), [number]); // toggling the theme now re-renders instantly — the prime isn't recomputed
- The symptom it cures: a heavy calc runs on an unrelated state change (theme toggle), freezing the UI for seconds. Memoizing it makes the unrelated update instant because the cached value is reused.
- The dependency array is the whole point:
[number]means "recompute only when number changes." On any other re-render, return the cached value. Get the deps wrong and you either over-compute or serve stale results. - Don't memo everything: if the calculation is cheap,
useMemoadds overhead for no gain. Reach for it only when you've identified a genuinely expensive operation — premature memoization is a real anti-pattern. - It doesn't stop re-renders: the component still re-renders;
useMemojust skips redoing the expensive work inside it.
useCallback — cache a function definition#
Nearly identical to useMemo, but it caches a function
instead of a computed value. Because every render recreates inline functions, a memoized callback
keeps the same reference across renders — useful for skipping re-renders of memoized children.
// useMemo caches a VALUE; useCallback caches a FUNCTION reference
const handleClick = useCallback(() => doThing(id), [id]);- The one-line distinction:
useMemo→ "cache the result of a calculation";useCallback→ "cache the function definition itself." Same dependency-array mechanics. - Why a stable reference matters: passing a fresh function to a memoized child every render defeats the child's memoization. A stable callback lets the child actually skip re-rendering.
- Same caveat as useMemo: only worth it on hot paths or when feeding memoized children — not by default.
useRef — persist a value without re-rendering#
Sometimes you want a value that survives re-renders (unlike a let) but doesn't trigger a re-render when it changes (unlike state).
That's exactly useRef — it returns an object { current } you mutate directly.
// useRef returns an object, not a bare value: { current: initial } const ref = useRef(0); ref.current = ref.current + 1; // mutate directly — no setter, no re-render // classic use: hold a timer id so a button can clear it later const timerId = useRef(null); useEffect(() => { timerId.current = setInterval(tick, 1000); return () => clearInterval(timerId.current); // cleanup on unmount }, []); // a "Stop" button elsewhere can call clearInterval(timerId.current)
- The three-way distinction (the money question): a
letis recreated every render;useStatepersists and re-renders on change;useRefpersists but does not re-render. Knowing all three cold is what interviewers fish for. - It's an object with
.current:useRef(0)gives you{ current: 0 }, not0. You read and mutateref.currentdirectly — there's no setter. - Why a
letfails for a timer id: React warns that a value assigned to a plain variable inside a hook is lost after each render. A ref is the correct home for things like interval ids you need across renders but don't render. - Honest framing:
useRefis rarely needed in everyday app code — say so. But being able to explain why it exists (persist without rendering) is a strong signal.
A ref is a sticky note on the monitor. You can scribble on it and change it anytime, and it stays put when the screen redraws — but changing it doesn't cause the screen to redraw. State is a note that also rings a bell to refresh the screen; a ref is a silent note.
Process & Approach#
Q.How do you start a machine-coding round when handed "build YouTube"?
Q.Why discuss the app's features when the interviewer already knows the product?
Q.How much time should you spend before writing code?
Q.You can't finish all of YouTube in two hours — what do you do?
Tech Stack & Justification#
Q.Why Redux over Context for an app like YouTube?
Q.Why Tailwind for styling?
Q.What does it mean to think in a UI layer and a data layer?
Q.What if the interviewer rejects your stack choice?
Q.Stack topics most candidates forget to mention?
Planning & Execution#
Q.What's the difference between HLD and LLD here?
/watch). LLD is the component tree — the named
components and their nesting. HLD decides the screens; LLD decides the files you'll create.
Q.Why build the skeleton before any feature?
Q.Is it OK to use create-react-app instead of configuring a bundler?
Q.What does npx create-react-app
actually do?
npx executes an npm package a single time without a global
install; create-react-app is the generator package that scaffolds a new
React project (folder structure, bundler, scripts, testing). Explaining this casually signals
real understanding.Q.How do you open a specific video on the watch page?
/watch?v=ID. On the watch page, read
the id with useSearchParams().get("v"), then embed the player (an
iframe) or call the video API with that id. Keeping it driven by the URL makes it shareable and
dynamic.Classic Patterns#
Q.What is a higher-order component?
Q.How do you stop a search bar from calling the API on every keystroke?
useEffect
on that state start a setTimeout (e.g. 200ms) to call the API —
with a cleanup that clearTimeouts the previous timer. Fast typing keeps
cancelling the pending timer; only a pause lets a call actually fire, collapsing many calls into
a few.Q.Why does the useEffect cleanup make
debouncing work?
Q.How would you cache search results, and what data structure?
cache[query]: that lookup is
O(1), versus O(n) scanning an array with includes/indexOf. On a hit, set suggestions from cache and skip the call entirely.
Mention bounding it with an LRU/FIFO cap for long sessions.Q.How do you build comments that nest to any depth?
{ name, text, replies: [] } where replies holds
more comments of the same shape. Then render recursively: a list component renders each comment,
and each comment renders the list component again for its replies. The self-call supports
unlimited depth. Use real ids as keys, not the index.Live Data & Performance#
Q.WebSockets vs API polling — how do you choose?
Q.Is YouTube live chat WebSockets or polling? And why?
get_live_chat calls roughly every 1.5 seconds. Chat doesn't need strict
ordering or millisecond latency, so polling is good enough and cheaper than holding millions of
open sockets. WhatsApp, by contrast, needs WebSockets because message order is critical.Q.How do you implement polling in React without leaking?
useEffect with an empty dependency array, start a
setInterval that fetches and dispatches the data into your store, and
return a cleanup that calls clearInterval. The empty array runs it once
after mount; the cleanup stops polling on unmount so you don't leak calls.
Q.A live feed keeps appending nodes — how do you stop the page freezing?
splice(LIMIT, 1) then unshift(newItem). The list size stays fixed however long the page is
open, so the DOM never bloats. This is what YouTube actually does (~250 messages).Q.Why does YouTube live chat not show timestamps?
Hooks & Re-renders#
Q.What triggers a re-render in React?
useState setter) or a prop change.
React then re-runs the component function from top to bottom — a fresh execution context
with all local variables recreated. Understanding this is the basis for every performance hook.
Q.Why use useState instead of a plain
let variable?
let changes in memory but doesn't trigger a re-render,
so the UI never updates — and it's reset to its initial value on the next render since the
function re-runs. useState is persisted by React outside that
per-render memory and re-renders the component when its setter is called.Q.What does useMemo do, and when do you
use it?
Q.Difference between useMemo and useCallback?
useMemo caches a computed value; useCallback caches a function reference. The mechanics
(dependency array) are identical. A stable function reference is useful when passing callbacks
to memoized child components so they can actually skip re-rendering.Q.let vs useState vs useRef — explain all three.
let is recreated on every render (doesn't persist,
doesn't re-render). useState persists across renders and
re-renders the component when changed. useRef persists across renders
but does not trigger a re-render — ideal for values like timer ids you need to
keep but don't display.Q.Why is useRef an object with .current?
useRef(initial) returns { current: initial } rather than a bare value, so React can hand you the
same mutable container across every render. You read and write ref.current directly — there's no setter and mutating it doesn't
re-render.