Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save carefree-ladka/467fff734af45a377228004800833a78 to your computer and use it in GitHub Desktop.

Select an option

Save carefree-ladka/467fff734af45a377228004800833a78 to your computer and use it in GitHub Desktop.
React Machine Coding Rounds — The Complete Playbook

React Machine Coding Rounds — The Complete Playbook

Companies don't just evaluate your code. They evaluate your thinking. This guide covers everything from round structure to the hidden rubric interviewers use — so you walk in knowing what's actually being measured.


Table of Contents

  1. Round Structure & Format
  2. Question Bank by Level
  3. The Hidden Evaluation Rubric
  4. Ideal Strategy — Step by Step
  5. Code Structure Principles
  6. State Management Patterns
  7. Hooks Mastery Checklist
  8. Edge Case Thinking
  9. Performance Signals
  10. Accessibility — The Biggest Differentiator
  11. Common Bug Patterns to Avoid
  12. Must-Know Concepts Before You Interview
  13. Difficulty Trend & Market Reality
  14. What Separates the Top 5%

1. Round Structure & Format

Most machine coding rounds run 60–120 minutes. The format is consistent across companies — understanding each phase lets you allocate time deliberately instead of guessing.

┌─────────────────────────────────────────────────────────────┐
│                    TYPICAL ROUND FLOW                       │
├──────────┬──────────────────────────────────────────────────┤
│  ~5 min  │ 1. Problem statement — read carefully            │
│ ~10 min  │ 2. Clarify requirements — ask smart questions    │
│ ~10 min  │ 3. Plan verbally — component tree, state, flow   │
│ ~50 min  │ 4. Implement core functionality                  │
│ ~15 min  │ 5. Edge cases + polish                           │
│ ~10 min  │ 6. Code review discussion with interviewer       │
└──────────┴──────────────────────────────────────────────────┘

The biggest mistake candidates make: spending 80% of time on the happy path and leaving zero time for edge cases. Interviewers notice this immediately.

What companies are NOT testing: Pixel-perfect UI, fancy animations, or framework knowledge breadth.

What they ARE testing: Engineering thinking, architectural decisions, correctness under edge cases, code quality, and performance awareness.


2. Question Bank by Level

⭐ Beginner–Mid Level — Fundamentals

These appear at startups and as warm-up rounds at product companies. They test that you understand React's core model correctly.

Question Core Concepts Tested
Todo app (CRUD + persistence) Controlled inputs, list rendering, localStorage
Search with debounce useEffect, useRef, race conditions, timing
Pagination component Derived state, data slicing, boundary math
Modal with accessibility Portal, focus trap, Escape key, aria-modal
Tabs component Controlled vs uncontrolled, keyboard nav
Infinite scroll list IntersectionObserver, async loading, deduplication
Form validation Error state, touched state, schema validation

⭐⭐ Mid Level — Architecture + State Logic

These are the most common across product companies. They separate candidates who know hooks from candidates who understand when and why to use them.

Question Core Concepts Tested
Nested comments system Recursive components, tree state, optimistic updates
Kanban board (drag & drop) Complex event handling, multi-list state, HTML5 DnD
File explorer UI Recursive tree, expand/collapse, path tracking
Data table — sort + filter Derived state, useMemo, controlled columns
Chat UI with typing indicator WebSocket/interval, cleanup, stale closure traps
Shopping cart with global state Context or Zustand, derived totals, persistence

⭐⭐⭐ Senior Level — System Thinking

These test whether you can design a subsystem, not just implement a feature. Expect to explain architectural tradeoffs during the review.

Question Core Concepts Tested
Mini Redux / store from scratch Pub-sub, reducer pattern, subscriptions, React integration
Virtualized list Windowing, DOM recycling, scroll math, accessibility
Rich text editor ContentEditable, Selection API, command pattern
Google Docs-like live typing Operational transforms vs CRDT concept, cursor sync
Real-time notifications panel WebSocket lifecycle, read/unread state, batching
Offline-first app Service Worker concept, sync queue, conflict resolution

3. The Hidden Evaluation Rubric

Interviewers rarely show you this. Here is what they are actually scoring against.

┌─────────────────────────────────────────────────────────────┐
│               HIDDEN EVALUATION SCORECARD                   │
├────────────────────────┬────────────────────────────────────┤
│ Code Structure         │ Separation of concerns, reusable   │
│                        │ components, clean folder layout     │
├────────────────────────┼────────────────────────────────────┤
│ State Management       │ Correct state location, no prop     │
│                        │ drilling, no derived state bugs     │
├────────────────────────┼────────────────────────────────────┤
│ Hooks Mastery          │ Right hook for right job, stable    │
│                        │ dependencies, custom hooks          │
├────────────────────────┼────────────────────────────────────┤
│ Edge Case Thinking     │ Empty, loading, error, race cond.  │
│                        │ rapid input, network failure        │
├────────────────────────┼────────────────────────────────────┤
│ Performance Awareness  │ Debounce, memo, key usage, lazy     │
│                        │ loading — mentioned OR implemented  │
├────────────────────────┼────────────────────────────────────┤
│ Accessibility          │ Keyboard nav, focus, aria — huge   │
│                        │ differentiator, almost no one does │
├────────────────────────┼────────────────────────────────────┤
│ Communication          │ Explains decisions while coding.   │
│                        │ Justifies tradeoffs. Asks questions.│
└────────────────────────┴────────────────────────────────────┘

A common surprise: a partially complete solution with clean architecture scores higher than a complete solution with messy code. Interviewers can extrapolate from good structure. They cannot unsee bad architecture.


4. Ideal Strategy — Step by Step

Step 1 — Clarify (Never Skip This)

Before writing a single line, ask requirement questions. This signals product thinking and prevents wasted implementation time.

Always ask:

  • What is the expected data size? (Affects virtualization decision)
  • Is mobile responsiveness required?
  • Is persistence needed (localStorage, API)?
  • Is accessibility explicitly required?
  • Should I use a specific state management approach, or is that my call?
  • Are there any API mocks provided, or should I stub my own?

Interviewers score this highly. Most candidates skip it and dive into code. That alone differentiates you.

Step 2 — Plan Verbally Before Coding

Spend 5–10 minutes talking through your plan. Draw the component tree. Narrate the state model.

"I'll break this into three components:
  - TodoList (container, owns state)
  - TodoItem (display + inline edit)
  - AddTodo (controlled form)

State lives in TodoList — items array. I'll derive filtered items
with useMemo rather than storing a separate filtered array.
Persistence goes into a custom useTodos hook."

This serves two purposes: it catches architectural mistakes before you've committed to code, and it demonstrates staff-level communication.

Step 3 — Build Core First, Polish Last

Follow this strict ordering:

Phase 1: Minimal working feature
  └── Core happy path only. No loading states, no errors yet.
      Get something on screen that actually works.

Phase 2: Edge cases
  └── Empty state, loading state, error state, rapid input,
      API failure. These reveal engineering maturity.

Phase 3: Polish (only if time remains)
  └── Loading spinner, keyboard support, aria attributes,
      animations. Nice to have, not required to pass.

Never start with styling. It signals wrong priorities and burns time you need for architecture.

Step 4 — Code Review Discussion

When the coding phase ends, be ready to:

  • Explain why each major decision was made
  • Identify what you would do differently with more time
  • Proactively name the known limitations of your implementation
  • Suggest how you would scale it (test coverage, API integration, performance)

The candidate who says "I'd add virtualization here if the list can grow past ~500 items — I didn't implement it given time constraints, but the hook boundary I drew makes it straightforward to add" impresses more than the one who silently waits.


5. Code Structure Principles

Folder Structure for Interview Projects

Even in a 60-minute round, structure your files intentionally. Interviewers look at your folder layout before they read your code.

src/
  components/
    TodoItem/
      TodoItem.jsx      ← Component
      TodoItem.css      ← Scoped styles (or module)
      index.js          ← Re-export
  hooks/
    useTodos.js         ← Custom hook for state logic
    useDebounce.js      ← Reusable utility hook
  utils/
    storage.js          ← localStorage helpers
    validators.js       ← Pure validation functions
  context/
    TodoContext.jsx     ← Context + Provider (if needed)
  App.jsx

Separation of Concerns

Keep three types of logic clearly separated:

UI logic — How things look and respond to events. Lives in components. Should be thin.

State logic — How data changes over time. Lives in custom hooks. Should be pure and testable.

Side effects — Persistence, API calls, subscriptions. Lives in hooks, isolated behind an interface.

// ❌ Mixed concerns — hard to test, hard to reuse
function TodoList() {
  const [todos, setTodos] = useState(() =>
    JSON.parse(localStorage.getItem('todos') || '[]')
  )
  // ...100 more lines of logic mixed with JSX
}

// ✅ Separated — the hook can be tested independently
function TodoList() {
  const { todos, addTodo, removeTodo, toggleTodo } = useTodos()
  return (/* pure JSX */)
}

function useTodos() {
  const [todos, setTodos] = useState(() =>
    JSON.parse(localStorage.getItem('todos') || '[]')
  )
  useEffect(() => {
    localStorage.setItem('todos', JSON.stringify(todos))
  }, [todos])
  // return stable action handlers
}

Reusable Components

Design components with a clear interface. Ask: "Could this component work in a different feature without modification?"

// ❌ Too coupled to one use case
function TodoSearchBar({ onTodoSearch }) { ... }

// ✅ General-purpose, reusable
function SearchBar({ placeholder, onSearch, debounceMs = 300 }) { ... }

6. State Management Patterns

Correct State Location — The Decision Tree

Ask: Who needs this state?

Only this component?
  └─► useState inside the component

This component + immediate children?
  └─► useState in the parent, pass as props

Multiple distant components?
  └─► Context (for low-churn state)
      or Zustand/external store (for high-churn state)

Server data (API responses)?
  └─► React Query / SWR — not useState + useEffect

Derived State — The Most Common Mistake

Never store data you can compute from existing state. Derived state causes sync bugs.

// ❌ Storing derived state — completedCount can go stale
const [todos, setTodos] = useState([])
const [completedCount, setCompletedCount] = useState(0)

// ✅ Derive it — always accurate, no sync required
const [todos, setTodos] = useState([])
const completedCount = useMemo(
  () => todos.filter(t => t.done).length,
  [todos]
)

Avoiding Prop Drilling

If you find yourself passing props more than 2 levels deep, extract a context:

// ❌ Prop drilling — passing theme through 4 layers
<App theme={theme}>
  <Layout theme={theme}>
    <Sidebar theme={theme}>
      <NavItem theme={theme} />

// ✅ Context — NavItem reads directly
const ThemeContext = createContext()
<ThemeContext.Provider value={theme}>
  <App /> {/* no prop threading */}

Global State with useReducer + Context

For mid-complexity state in interview projects, useReducer + Context is the right answer. It shows architectural thinking without requiring external libraries.

// store/todoReducer.js
const initialState = { todos: [], filter: 'all' }

function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return { ...state, todos: [...state.todos, action.payload] }
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map(t =>
          t.id === action.payload ? { ...t, done: !t.done } : t
        )
      }
    case 'SET_FILTER':
      return { ...state, filter: action.payload }
    default:
      return state
  }
}

// context/TodoContext.jsx
const TodoContext = createContext()

export function TodoProvider({ children }) {
  const [state, dispatch] = useReducer(todoReducer, initialState)
  return (
    <TodoContext.Provider value={{ state, dispatch }}>
      {children}
    </TodoContext.Provider>
  )
}

export const useTodoStore = () => useContext(TodoContext)

7. Hooks Mastery Checklist

Interviewers watch not just whether you use hooks, but whether you use the right hook for the right problem.

useState vs useRef — When to Choose Which

Ask: Does this value changing need to trigger a re-render?

YES → useState
NO  → useRef
// useRef for: timers, previous values, DOM nodes, interval IDs
const timerRef = useRef(null)
const prevValueRef = useRef(value)
const inputRef = useRef(null)

// useState for: anything the UI needs to reflect
const [count, setCount] = useState(0)
const [isOpen, setIsOpen] = useState(false)

useMemo — When It's Worth It

Only memoize expensive computations. The memoization itself has a cost.

// ❌ Useless — primitive comparison is already cheap
const doubled = useMemo(() => count * 2, [count])

// ✅ Worthwhile — filters/sorts over large arrays
const sortedUsers = useMemo(
  () => [...users].sort((a, b) => a.name.localeCompare(b.name)),
  [users]
)

// ✅ Required — prevents re-render of memoized child
const config = useMemo(() => ({ theme, locale }), [theme, locale])
<MemoizedChild config={config} />

useCallback — The Common Misconception

useCallback is not for performance of the function itself — it is for stabilizing the function reference so memoized children do not re-render unnecessarily.

// ❌ Pointless — this component is not memoized
function Parent() {
  const handleClick = useCallback(() => doThing(), []) // wasted
  return <button onClick={handleClick}>Click</button>
}

// ✅ Required — MemoizedChild uses React.memo, needs stable ref
function Parent() {
  const handleClick = useCallback(() => doThing(), [])
  return <MemoizedChild onClick={handleClick} />
}

Custom Hooks — Extract Early

Any logic that involves hooks and could be reused or tested independently should become a custom hook. This is one of the strongest signals of seniority in a coding round.

// useDebounce — reusable across search, autocomplete, etc.
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value)

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay)
    return () => clearTimeout(timer)  // cleanup on every change
  }, [value, delay])

  return debouncedValue
}

// usePrevious — useful for animations, change detection
function usePrevious(value) {
  const ref = useRef()
  useEffect(() => { ref.current = value })
  return ref.current
}

// useLocalStorage — persistence abstraction
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    try {
      const item = localStorage.getItem(key)
      return item ? JSON.parse(item) : initialValue
    } catch { return initialValue }
  })

  const setStoredValue = useCallback((newValue) => {
    setValue(newValue)
    localStorage.setItem(key, JSON.stringify(newValue))
  }, [key])

  return [value, setStoredValue]
}

Dependency Array — The Critical Rules

// Rule 1: Include everything you read from component scope
useEffect(() => {
  fetchUser(userId)          // userId must be in deps
}, [userId])                 // ✅

// Rule 2: Stable refs don't need to be in deps
useEffect(() => {
  inputRef.current.focus()   // refs are stable — exclude
}, [])                       // ✅

// Rule 3: Functions defined outside the effect need useCallback or inclusion
const fetchData = useCallback(() => { /* ... */ }, [id])
useEffect(() => { fetchData() }, [fetchData])  // ✅

// Rule 4: Empty array = run once on mount (not "no dependencies")
useEffect(() => {
  const sub = subscribe()
  return () => sub.unsubscribe()
}, [])  // ✅ — intentional one-time setup

8. Edge Case Thinking

This is where most candidates lose marks. Interviewers note whether you spontaneously handle edge cases or only build the happy path.

The Five States Every Feature Needs

Every async operation or data-dependent UI has 5 states:

1. IDLE      → Nothing has happened yet (initial render)
2. LOADING   → Waiting for data
3. SUCCESS   → Data received, display it
4. EMPTY     → Success, but no data to show
5. ERROR     → Something went wrong

Model these explicitly, never implicitly.
// ❌ Only handles success — fragile
function UserList() {
  const [users, setUsers] = useState([])
  useEffect(() => {
    fetch('/api/users').then(r => r.json()).then(setUsers)
  }, [])
  return users.map(u => <UserCard key={u.id} user={u} />)
}

// ✅ Handles all 5 states
function UserList() {
  const [users, setUsers] = useState([])
  const [status, setStatus] = useState('idle')
  const [error, setError] = useState(null)

  useEffect(() => {
    setStatus('loading')
    fetch('/api/users')
      .then(r => r.json())
      .then(data => {
        setUsers(data)
        setStatus('success')
      })
      .catch(err => {
        setError(err.message)
        setStatus('error')
      })
  }, [])

  if (status === 'loading') return <Spinner />
  if (status === 'error')   return <ErrorMessage message={error} />
  if (users.length === 0)   return <EmptyState message="No users found" />
  return users.map(u => <UserCard key={u.id} user={u} />)
}

Race Conditions in useEffect

When a user triggers multiple fetches in quick succession (typing in a search box), responses can arrive out of order. The last response to resolve wins — but that may not be the most recent request.

// ❌ Race condition — slow request for 'ab' might resolve after 'abc'
useEffect(() => {
  fetch(`/search?q=${query}`).then(r => r.json()).then(setResults)
}, [query])

// ✅ Cleanup flag pattern — ignores stale responses
useEffect(() => {
  let cancelled = false

  fetch(`/search?q=${query}`)
    .then(r => r.json())
    .then(data => {
      if (!cancelled) setResults(data)  // only set if still relevant
    })

  return () => { cancelled = true }  // cancel on query change
}, [query])

// ✅ AbortController pattern — actually cancels the fetch
useEffect(() => {
  const controller = new AbortController()

  fetch(`/search?q=${query}`, { signal: controller.signal })
    .then(r => r.json())
    .then(setResults)
    .catch(err => {
      if (err.name !== 'AbortError') setError(err)
    })

  return () => controller.abort()
}, [query])

Rapid Input Handling

// Always debounce search inputs — never fire on every keystroke
function SearchInput({ onSearch }) {
  const [value, setValue] = useState('')
  const debouncedValue = useDebounce(value, 300)

  useEffect(() => {
    if (debouncedValue) onSearch(debouncedValue)
  }, [debouncedValue, onSearch])

  return (
    <input
      value={value}
      onChange={e => setValue(e.target.value)}
      placeholder="Search..."
    />
  )
}

Stale Closure Bug

// ❌ Stale closure — count is captured at interval creation time
useEffect(() => {
  const id = setInterval(() => {
    setCount(count + 1)  // always adds to the initial value of count
  }, 1000)
  return () => clearInterval(id)
}, [])  // count not in deps — stale!

// ✅ Functional update — uses current state, not captured value
useEffect(() => {
  const id = setInterval(() => {
    setCount(c => c + 1)  // c is always current
  }, 1000)
  return () => clearInterval(id)
}, [])

9. Performance Signals

You do not need to implement all optimizations in a 60-minute round. But you must mention them when relevant, and explain the tradeoff. That is the signal interviewers listen for.

Debouncing vs Throttling

Debounce  → Fire AFTER user stops. Use for: search, autocomplete, save on type.
Throttle  → Fire AT MOST every N ms. Use for: scroll, resize, mousemove.
// Debounce implementation (common interview sub-question)
function debounce(fn, delay) {
  let timer
  return function(...args) {
    clearTimeout(timer)
    timer = setTimeout(() => fn.apply(this, args), delay)
  }
}

// Throttle implementation
function throttle(fn, limit) {
  let lastCall = 0
  return function(...args) {
    const now = Date.now()
    if (now - lastCall >= limit) {
      lastCall = now
      return fn.apply(this, args)
    }
  }
}

Memoization Strategy

// React.memo — prevent re-render when parent re-renders
const UserCard = React.memo(function UserCard({ user }) {
  return <div>{user.name}</div>
})

// Only apply when:
// 1. Component renders often due to parent updates
// 2. Component is expensive to render
// 3. Props don't change often
// Don't apply blindly — memo has overhead too

Key Usage for Performance

// ✅ Good key — stable ID from data
{todos.map(todo => <TodoItem key={todo.id} todo={todo} />)}

// When to use index as key (the only acceptable case):
// - List is never reordered
// - List items have no persistent state
// - List is never filtered
{staticLabels.map((label, i) => <span key={i}>{label}</span>)}

When to Mention Virtualization

If data size could exceed ~100–200 items, mention virtualization proactively:

"For this implementation I'm rendering all items, which works fine up to a few hundred. If this list can grow to thousands, I'd swap in react-window — the API is similar but only renders visible DOM nodes."

That sentence alone signals seniority.


10. Accessibility — The Biggest Differentiator

This is mentioned last by most guides but should be near the top of your implementation list. Fewer than 10% of candidates implement even basic accessibility. Doing so is an instant strong signal.

Modal — The Most Complete Accessibility Example

A modal requires four accessibility behaviors: focus trap, Escape key close, aria attributes, and scroll lock.

function Modal({ isOpen, onClose, title, children }) {
  const overlayRef = useRef(null)
  const closeButtonRef = useRef(null)

  // Focus the close button when modal opens
  useEffect(() => {
    if (isOpen) closeButtonRef.current?.focus()
  }, [isOpen])

  // Escape key closes modal
  useEffect(() => {
    if (!isOpen) return
    const handler = (e) => { if (e.key === 'Escape') onClose() }
    document.addEventListener('keydown', handler)
    return () => document.removeEventListener('keydown', handler)
  }, [isOpen, onClose])

  // Focus trap — Tab cycles within modal only
  const handleKeyDown = (e) => {
    if (e.key !== 'Tab') return
    const focusable = overlayRef.current.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    )
    const first = focusable[0]
    const last = focusable[focusable.length - 1]
    if (e.shiftKey && document.activeElement === first) {
      e.preventDefault()
      last.focus()
    } else if (!e.shiftKey && document.activeElement === last) {
      e.preventDefault()
      first.focus()
    }
  }

  if (!isOpen) return null

  return createPortal(
    <div
      role="dialog"
      aria-modal="true"
      aria-labelledby="modal-title"
      ref={overlayRef}
      onKeyDown={handleKeyDown}
      style={{ /* overlay styles */ }}
    >
      <div>
        <h2 id="modal-title">{title}</h2>
        {children}
        <button ref={closeButtonRef} onClick={onClose}>
          Close
        </button>
      </div>
    </div>,
    document.body
  )
}

Keyboard Navigation for Lists

function TodoList({ todos }) {
  const handleKeyDown = (e, id) => {
    if (e.key === 'Enter' || e.key === ' ') {
      e.preventDefault()
      toggleTodo(id)
    }
    if (e.key === 'Delete') deleteTodo(id)
  }

  return (
    <ul role="list">
      {todos.map(todo => (
        <li
          key={todo.id}
          role="checkbox"
          aria-checked={todo.done}
          tabIndex={0}
          onKeyDown={(e) => handleKeyDown(e, todo.id)}
          onClick={() => toggleTodo(todo.id)}
        >
          {todo.text}
        </li>
      ))}
    </ul>
  )
}

Aria Quick Reference for Common Components

Component Key ARIA Attributes
Modal role="dialog", aria-modal="true", aria-labelledby
Button (icon only) aria-label="Close"
Loading spinner role="status", aria-live="polite", aria-label="Loading"
Search input role="search", aria-label, aria-controls for results
Tabs role="tablist", role="tab", aria-selected, aria-controls
Checkbox role="checkbox", aria-checked
Alert/Error role="alert", aria-live="assertive"

11. Common Bug Patterns to Avoid

These are the bugs interviewers plant or look for. Knowing them in advance prevents embarrassing moments.

Infinite useEffect Loop

// ❌ Infinite loop — data changes → effect runs → fetches → sets data → repeat
useEffect(() => {
  fetch('/api/items')
    .then(r => r.json())
    .then(items => setData({ ...data, items }))  // new object ref every time
}, [data])  // data in deps = triggers on every data change

// ✅ Specific dependency — only runs when id changes
useEffect(() => {
  fetch(`/api/items/${id}`).then(r => r.json()).then(setItems)
}, [id])

Mutating State Directly

// ❌ Direct mutation — React cannot detect the change
const handleAdd = (item) => {
  todos.push(item)  // mutates existing array
  setTodos(todos)   // same reference — React bails out, no re-render
}

// ✅ New array — React detects the change
const handleAdd = (item) => {
  setTodos(prev => [...prev, item])
}

Missing Cleanup in useEffect

// ❌ Memory leak — subscription never removed when component unmounts
useEffect(() => {
  const subscription = eventBus.subscribe('update', handleUpdate)
}, [])

// ✅ Cleanup function returned — runs on unmount
useEffect(() => {
  const subscription = eventBus.subscribe('update', handleUpdate)
  return () => subscription.unsubscribe()
}, [])

Object Spread Missing Nested Updates

// ❌ Shallow spread — nested object mutation survives
const updateUserCity = (city) => {
  setUser({ ...user, address: { city } })  // drops country, zip, etc.
}

// ✅ Spread at every level
const updateUserCity = (city) => {
  setUser(prev => ({
    ...prev,
    address: { ...prev.address, city }
  }))
}

12. Must-Know Concepts Before You Interview

If you can answer these confidently, you can handle 90% of machine coding rounds.

Controlled vs Uncontrolled Components — Controlled components store form state in React state (you control every keystroke). Uncontrolled components let the DOM own the state and you read it via ref. Know when each is appropriate and the tradeoffs.

Reconciliation & Keys — React matches children by key during diffing. Without keys, it matches by position, which breaks on reorder. With stable keys, it reuses existing fiber nodes and DOM elements.

Closure Pitfalls — Event handlers and effects capture the value of state variables at the time they are created. If the state changes, the old closure has the old value. Fix with functional updates (setState(prev => ...)) or refs.

Stale State Bug Pattern — Reading state inside a setTimeout or setInterval gets the value at capture time, not current time. Use refs to read current state inside timers.

Rendering Lifecycle — Render (pure, can be interrupted) → Commit (DOM mutations) → Layout effects → Paint → Passive effects. Know what runs when and why.

Batching — React 18 batches all state updates by default, including those in Promises and setTimeout. Multiple setState calls in one event produce one re-render. Before React 18, only event handler updates were batched.

Concurrent Rendering Basics — React can pause low-priority renders to handle urgent work. startTransition marks an update as non-urgent. useDeferredValue defers the update of a derived value. Both prevent UI jank during heavy updates.


13. Difficulty Trend & Market Reality

The bar has risen significantly and continues to rise.

Year Expected Level
2022 Medium — implement the feature correctly
2023 Medium–Hard — correct + performant
2024 Hard — correct + performant + architectural thinking
2025+ Hard + system thinking — tradeoffs, scale, team implications

The practical implication: What a mid-level engineer was expected to know in 2022 is now the floor for any engineer asking for a senior title. The delta is system thinking, not additional syntax knowledge.

Companies now expect mid-level candidates to demonstrate knowledge that was previously only tested at senior level. If you are targeting senior compensation, you need to prepare at the staff level.


14. What Separates the Top 5%

The top 5% of candidates share one consistent behavior: they talk while they code.

They do not just type. They narrate decisions as they make them, in real time.

While writing the state model:
  "I'm putting this in the parent rather than the child because
   the sibling component also needs to read it."

While adding useMemo:
  "I'm memoizing this sort because it runs over the full list on
   every render — without memo, every keystroke re-sorts."

While skipping an optimization:
  "I'd normally debounce this, but since the dataset is local
   and small, I'll note the omission and skip it to save time."

Interviewers are not just evaluating your code. They are evaluating your thinking. The only way to demonstrate thinking is to externalize it.

What this communicates:

  • You understand why you are making each decision, not just how
  • You are aware of the tradeoffs you are choosing to accept
  • You would be easy to work with and code review with
  • You operate like someone who designs systems, not just writes features

The candidate who says "why state lives here", "why memo is needed", "why ref instead of state" — and gets the answers right — gets the offer.


The machine coding round is not a test of how fast you type. It is a structured window into how you think. Prepare accordingly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment