Skip to content

Machine Coding Problems (Uber R3) ​

The machine coding round is the differentiator at Uber. R3 is where most candidates get sorted into "good enough" vs "actually hired." Uber favors vanilla JS over framework-specific magic. Expect the interviewer to watch you build something end-to-end in 45-60 minutes, then probe with follow-ups.

Machine Coding Interview Strategy ​

You have about 45 minutes. That is not a lot of time. The candidates who pass do the same few things.

Start vanilla, not framework-first. Uber interviewers often care more about your JS fundamentals than your React fluency. Even when you use React, start by sketching the vanilla building blocks (a pure data model, a render function, event handlers) and extract them into utilities. That signals you think in layers, not just in hooks.

Declare your plan aloud before coding. Spend the first 5 minutes explaining:

  • The data shape you're going to hold
  • The two or three functions you'll expose
  • The DOM or component structure you'll render
  • Which edge cases you'll handle first vs last

This protects you: if the interviewer disagrees with your approach, you course-correct before writing code.

Build incrementally. Get the happy path working on screen in the first 15 minutes. Then iterate. A half-working demo beats a perfect design living in your head.

Test each feature as you go. Click it. Log it. Don't wait until the end.

Handle 1-2 edge cases explicitly. Mention the rest. You won't have time for every edge case, but naming them shows you see them.

Know ES6 modular patterns. Uber often asks you to split logic across files using import and export. Practice setting up a one-file-per-concern structure quickly.

When you finish, do not stop talking. Summarize what you built, list the follow-ups you'd add with more time, and wait for the interviewer to pick the next direction.


Grid Light Box (Reverse Deactivation) ​

Reported Frequency: Most cited Uber FE question 2024-2025. Appears in roughly 1 in 3 reported L5A R3 loops.

Problem ​

You have an N by N grid of empty boxes. When the user clicks a box, it lights up (turns green). After the user clicks the last remaining unlit box, all boxes deactivate one by one, 300ms apart, in reverse order of activation (LIFO). Once the deactivation animation starts, clicks should be disabled until the grid resets.

Follow-ups you'll hear:

  • Make grid size configurable at runtime.
  • Add a reset button that cancels any in-flight deactivation.
  • What if a user can click a box twice? Should the second click bump it to the end of the stack?

Key Design Decisions ​

  • Hold activation order in an array (used as a stack). Pushing on activate, popping on deactivate gives LIFO for free.
  • Track "locked" state separately from the grid so clicks during deactivation are no-ops without data loss.
  • Use setTimeout chained in a promise loop so you can cancel via a token.

Solution - Vanilla JS ​

javascript
// lightbox.js
export function createLightBox(root, size = 3) {
  const state = {
    activated: [],              // stack of indices in order clicked
    locked: false,
    cancelToken: { canceled: false },
  };

  const cells = [];
  root.innerHTML = "";
  root.style.display = "grid";
  root.style.gridTemplateColumns = `repeat(${size}, 60px)`;
  root.style.gap = "4px";

  for (let i = 0; i < size * size; i++) {
    const cell = document.createElement("button");
    cell.type = "button";
    cell.dataset.index = String(i);
    cell.className = "cell";
    cell.style.height = "60px";
    cell.style.background = "white";
    cell.style.border = "1px solid #ccc";
    cell.addEventListener("click", () => onClick(i));
    root.appendChild(cell);
    cells.push(cell);
  }

  function onClick(index) {
    if (state.locked) return;
    if (state.activated.includes(index)) return;

    state.activated.push(index);
    cells[index].style.background = "#16a34a";

    if (state.activated.length === cells.length) {
      startDeactivation();
    }
  }

  async function startDeactivation() {
    state.locked = true;
    const token = state.cancelToken;

    while (state.activated.length > 0) {
      if (token.canceled) return;
      await wait(300);
      if (token.canceled) return;
      const last = state.activated.pop();
      cells[last].style.background = "white";
    }
    state.locked = false;
  }

  function wait(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  function reset() {
    // cancel any in-flight deactivation
    state.cancelToken.canceled = true;
    state.cancelToken = { canceled: false };
    state.activated = [];
    state.locked = false;
    cells.forEach((c) => (c.style.background = "white"));
  }

  return { reset };
}

// usage
// const api = createLightBox(document.getElementById("root"), 3);
// document.getElementById("reset").addEventListener("click", api.reset);

Solution - React ​

tsx
import { useCallback, useRef, useState } from "react";

type Props = { size?: number };

export function LightBox({ size = 3 }: Props) {
  const total = size * size;
  const [active, setActive] = useState<number[]>([]);
  const [locked, setLocked] = useState(false);
  const cancelRef = useRef({ canceled: false });

  const startDeactivation = useCallback(async (stack: number[]) => {
    setLocked(true);
    const token = cancelRef.current;
    const copy = [...stack];
    while (copy.length > 0) {
      await new Promise((r) => setTimeout(r, 300));
      if (token.canceled) return;
      copy.pop();
      setActive([...copy]);
    }
    setLocked(false);
  }, []);

  const onClick = (i: number) => {
    if (locked || active.includes(i)) return;
    const next = [...active, i];
    setActive(next);
    if (next.length === total) startDeactivation(next);
  };

  const reset = () => {
    cancelRef.current.canceled = true;
    cancelRef.current = { canceled: false };
    setActive([]);
    setLocked(false);
  };

  return (
    <>
      <div
        style={{
          display: "grid",
          gridTemplateColumns: `repeat(${size}, 60px)`,
          gap: 4,
        }}
      >
        {Array.from({ length: total }).map((_, i) => (
          <button
            key={i}
            onClick={() => onClick(i)}
            style={{
              height: 60,
              background: active.includes(i) ? "#16a34a" : "white",
              border: "1px solid #ccc",
            }}
          />
        ))}
      </div>
      <button onClick={reset}>Reset</button>
    </>
  );
}

Edge Cases and Gotchas ​

  • Clicking an already-lit cell should not double-activate it. Check membership before pushing.
  • During deactivation, clicks must be ignored but not queued.
  • If the user double-clicks fast, the second click must not sneak through while setLocked(true) is batching.
  • Resetting mid-animation should stop the loop. Use a token object the loop checks.

Follow-ups to Expect ​

  • "What if cells should pulse on activation?" Add a CSS keyframe class toggled via classList.add.
  • "Make it 1000 cells." Switch to a single event listener on the grid using event delegation (read dataset.index).
  • "Persist state across refresh." Serialize activated to localStorage on every mutation; hydrate in the constructor.

What They're Testing ​

State ownership, stack semantics, the difference between UI lock and data. Bonus signal: you recognize the cancelable async loop pattern without being told.


Progress Bar with Throttle ​

Reported Frequency: High. Appears in both L4 and L5A loops.

Problem ​

Click a button, a new progress bar appears and fills from 0 to 100 percent over 3 seconds. Multiple clicks stack up bars. Follow-up: cap concurrent fills at 3. A fourth click should queue until a slot opens.

Key Design Decisions ​

  • Drive animation with CSS transition, not JS setInterval. The browser handles the 60fps rendering for free.
  • Represent bars as an array of objects with a state machine (queued | running | done).
  • Use a simple FIFO queue plus a running-count so scheduling is deterministic.

Solution - Vanilla JS ​

javascript
// progress.js
export function createProgressManager(container, maxConcurrent = 3, durationMs = 3000) {
  const bars = [];
  let running = 0;
  const queue = [];

  function add() {
    const id = bars.length;
    const el = renderBar(id);
    container.appendChild(el);
    const bar = { id, el, state: "queued" };
    bars.push(bar);

    queue.push(bar);
    pump();
  }

  function pump() {
    while (running < maxConcurrent && queue.length > 0) {
      const bar = queue.shift();
      start(bar);
    }
  }

  function start(bar) {
    running++;
    bar.state = "running";
    const fill = bar.el.querySelector(".fill");
    // force reflow so transition is picked up
    fill.getBoundingClientRect();
    fill.style.transition = `width ${durationMs}ms linear`;
    fill.style.width = "100%";
    setTimeout(() => {
      bar.state = "done";
      running--;
      pump();
    }, durationMs);
  }

  function renderBar(id) {
    const wrap = document.createElement("div");
    wrap.style.cssText =
      "height:12px;background:#eee;border-radius:6px;overflow:hidden;margin:6px 0;";
    wrap.innerHTML = `<div class="fill" style="height:100%;width:0;background:#2563eb;"></div>`;
    return wrap;
  }

  return { add };
}

// usage
// const mgr = createProgressManager(document.getElementById("bars"), 3, 3000);
// document.getElementById("add").addEventListener("click", mgr.add);

Edge Cases and Gotchas ​

  • Forgetting the reflow between width: 0 and width: 100% causes the transition to skip entirely.
  • setInterval driven approaches drift over time and look janky. CSS transition is both simpler and smoother.
  • If the tab is backgrounded, setTimeout fires late. That's fine here but would matter for time-critical work.

Follow-ups to Expect ​

  • "Support pause / resume." Use getComputedStyle to read current width, then set it as the new starting width and recompute remaining duration.
  • "Reset all." Iterate bars, call el.remove(), clear queue, reset counter.
  • "Persist progress on refresh." Save per-bar start timestamp; on hydrate compute elapsed.

What They're Testing ​

Do you know that layout animation belongs in CSS. Do you understand FIFO queuing with a max-concurrent bound (this is the same pattern as request throttling).


Sequential Bind / Unbind Click ​

Reported Frequency: Mid. Frequent when interviewer is senior FE.

Problem ​

Build a utility such that each click runs an async handler, and the next click is blocked until the current handler resolves. Provide bind(el, handler) and unbind(el). Support multiple elements independently. Split into ES6 modules.

Solution - Vanilla JS ​

javascript
// seq-click.js
const registry = new WeakMap();

export function bind(el, handler) {
  if (registry.has(el)) unbind(el);

  const record = {
    running: false,
    listener: async (e) => {
      if (record.running) return;
      record.running = true;
      try {
        await handler(e);
      } finally {
        record.running = false;
      }
    },
  };

  el.addEventListener("click", record.listener);
  registry.set(el, record);
}

export function unbind(el) {
  const record = registry.get(el);
  if (!record) return;
  el.removeEventListener("click", record.listener);
  registry.delete(el);
}
javascript
// main.js
import { bind, unbind } from "./seq-click.js";

const btn = document.getElementById("submit");

bind(btn, async () => {
  btn.textContent = "Saving...";
  await fetch("/save", { method: "POST" });
  btn.textContent = "Save";
});

Edge Cases and Gotchas ​

  • Rebinding should replace, not stack. The WeakMap + unbind on re-bind guarantees this.
  • Handler throws? Use try/finally so the lock always releases.
  • Multiple elements must not share a lock. WeakMap keyed on element gives isolation.

Follow-ups to Expect ​

  • "What if we want to queue clicks instead of drop?" Swap the if (running) return for a promise chain where you await the previous and then run.
  • "What about unmount?" WeakMap allows the record to be garbage collected if the element is gone, but the listener itself still needs removeEventListener.

What They're Testing ​

Do you reach for WeakMap when keyed state should not leak. Do you understand async locks.


Batch Data Utility ​

Reported Frequency: High. Classic Uber utility question.

Problem ​

Implement createBatcher(batchSize, timeoutMs, sendFn). Calls to push(item) accumulate. When the batch reaches batchSize, flush immediately. Otherwise flush when timeoutMs elapses since the last push. Each flush resets both the buffer and the timer.

Solution - TypeScript ​

typescript
type Sender<T> = (items: T[]) => void | Promise<void>;

export function createBatcher<T>(
  batchSize: number,
  timeoutMs: number,
  send: Sender<T>,
) {
  let buffer: T[] = [];
  let timer: ReturnType<typeof setTimeout> | null = null;

  function flush() {
    if (buffer.length === 0) return;
    const items = buffer;
    buffer = [];
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }
    send(items);
  }

  function push(item: T) {
    buffer.push(item);

    if (buffer.length >= batchSize) {
      flush();
      return;
    }

    // reset timer on every push so we flush `timeoutMs` after the LAST push
    if (timer) clearTimeout(timer);
    timer = setTimeout(flush, timeoutMs);
  }

  return { push, flush };
}

// usage
const batcher = createBatcher<number>(5, 200, (items) => console.log("send", items));
[1, 2, 3].forEach(batcher.push);
// after 200ms of idle → "send [1,2,3]"

Edge Cases and Gotchas ​

  • Clarify whether timer resets on every push ("sliding window") or starts on the first push after a flush ("fixed window"). Both are reasonable; Uber usually wants sliding.
  • flush while the timer is armed must clear it, or you'll double-fire.
  • What if send is async and throws? Decide if the batch is lost or retried. Mention this aloud.

Follow-ups to Expect ​

  • "Support retries with backoff." Wrap send in a retry helper.
  • "What if push is called during flush?" If send is sync, there is no race. If async, items pushed during the await go into the next batch - which is usually correct.
  • "Cap total buffered size to avoid OOM." Drop oldest or reject.

What They're Testing ​

Do you see the two triggers (size, time) and handle both without redundant state.


Rate Limiter Decorator ​

Reported Frequency: Mid-to-high. Common as a variant of debounce.

Problem ​

rateLimit(fn, { max, windowMs, mode }) where mode is "queue" or "drop". Allow at most max calls per rolling windowMs. Rolling means: look at timestamps within the past windowMs.

Solution - TypeScript ​

typescript
type Mode = "queue" | "drop";

export function rateLimit<T extends (...args: any[]) => any>(
  fn: T,
  opts: { max: number; windowMs: number; mode: Mode },
): (...args: Parameters<T>) => Promise<ReturnType<T> | null> {
  const { max, windowMs, mode } = opts;
  const timestamps: number[] = [];
  const queue: Array<{
    args: Parameters<T>;
    resolve: (v: ReturnType<T> | null) => void;
  }> = [];
  let draining = false;

  function prune(now: number) {
    while (timestamps.length && now - timestamps[0] >= windowMs) {
      timestamps.shift();
    }
  }

  async function drain() {
    if (draining) return;
    draining = true;
    while (queue.length) {
      const now = Date.now();
      prune(now);
      if (timestamps.length >= max) {
        const wait = windowMs - (now - timestamps[0]);
        await new Promise((r) => setTimeout(r, wait));
        continue;
      }
      const job = queue.shift()!;
      timestamps.push(Date.now());
      job.resolve(fn(...job.args));
    }
    draining = false;
  }

  return (...args: Parameters<T>) => {
    const now = Date.now();
    prune(now);

    if (timestamps.length < max) {
      timestamps.push(now);
      return Promise.resolve(fn(...args));
    }

    if (mode === "drop") return Promise.resolve(null);

    return new Promise<ReturnType<T> | null>((resolve) => {
      queue.push({ args, resolve });
      drain();
    });
  };
}

Edge Cases and Gotchas ​

  • "Rolling" vs "fixed" window matters. Pruning oldest timestamps gives rolling.
  • In queue mode, back-to-back drains can starve other work. The draining guard prevents nested drains.
  • fn may be async itself. Resolve with whatever fn returns.

Follow-ups to Expect ​

  • "Add jitter" — add small randomness to the wait to avoid thundering herds.
  • "Support per-key rate limits" (per user id). Key a Map<string, State>.

What They're Testing ​

Understanding of rolling windows, async queue draining, and the tradeoff between drop and queue semantics.


Memoize Async Function ​

Reported Frequency: High. Uber loves async primitives.

Problem ​

memoizeAsync(fn, { ttl }). Args are JSON-serializable. If called twice with the same args before the first resolves, both callers must get the same promise (request coalescing). Support optional TTL after which the cache entry expires.

Solution - TypeScript ​

typescript
type Entry<V> = { promise: Promise<V>; expiresAt: number };

export function memoizeAsync<A extends any[], V>(
  fn: (...args: A) => Promise<V>,
  opts: { ttl?: number } = {},
): (...args: A) => Promise<V> {
  const cache = new Map<string, Entry<V>>();

  return (...args: A): Promise<V> => {
    const key = JSON.stringify(args);
    const now = Date.now();
    const hit = cache.get(key);

    if (hit && hit.expiresAt > now) {
      return hit.promise;
    }

    const promise = fn(...args).catch((err) => {
      // don't cache failures
      cache.delete(key);
      throw err;
    });

    cache.set(key, {
      promise,
      expiresAt: opts.ttl ? now + opts.ttl : Infinity,
    });

    return promise;
  };
}

// usage
const getUser = memoizeAsync(
  async (id: string) => (await fetch(`/users/${id}`)).json(),
  { ttl: 60_000 },
);
await Promise.all([getUser("1"), getUser("1")]); // only one fetch

Edge Cases and Gotchas ​

  • Caching rejected promises is almost always wrong. Evict on failure.
  • JSON.stringify is not stable across object key order. If you need stability, sort keys before serializing.
  • TTL should be measured from cache insertion, not from promise resolution. Otherwise a slow resolve poisons the window.

Follow-ups to Expect ​

  • "Support weak references for cache-invalidation by object identity" — use WeakMap keyed on the argument object itself.
  • "Add max size." Wrap Map in a simple LRU.
  • "Expose invalidate(key)." Public method that does cache.delete(key).

What They're Testing ​

Do you de-dupe in-flight calls, not just completed ones. This is the signal that separates a real backend-aware frontend engineer from a copy-paste one.


Reported Frequency: Mid. Reported on Sept 2025 Bangalore L5A loop.

Problem ​

Build a modal system where each modal has a numeric priority. Opening a higher-priority modal automatically closes any open lower-priority modal. Lower priority modals opened while a higher one is open are queued (or silently dropped - clarify). Modals support title, body, primary action, secondary action, and a close icon.

Solution - React + TypeScript ​

tsx
import { createContext, useCallback, useContext, useMemo, useState } from "react";

type ModalSpec = {
  id: string;
  title: string;
  body: React.ReactNode;
  priority: number;
  primary?: { label: string; onClick: () => void };
  secondary?: { label: string; onClick: () => void };
};

type Ctx = {
  show: (m: ModalSpec) => void;
  hide: (id: string) => void;
};

const ModalContext = createContext<Ctx | null>(null);

export function ModalProvider({ children }: { children: React.ReactNode }) {
  const [stack, setStack] = useState<ModalSpec[]>([]);

  const show = useCallback((m: ModalSpec) => {
    setStack((prev) => {
      const current = prev[prev.length - 1];
      if (current && current.priority > m.priority) {
        // new one is lower priority; queue below
        return [m, ...prev];
      }
      // new one is higher or equal; push on top, close any lower-priority
      const filtered = prev.filter((x) => x.priority >= m.priority);
      return [...filtered, m];
    });
  }, []);

  const hide = useCallback((id: string) => {
    setStack((prev) => prev.filter((x) => x.id !== id));
  }, []);

  const value = useMemo(() => ({ show, hide }), [show, hide]);
  const top = stack[stack.length - 1];

  return (
    <ModalContext.Provider value={value}>
      {children}
      {top && (
        <div className="overlay" role="dialog" aria-modal="true" aria-labelledby={`${top.id}-title`}>
          <div className="modal">
            <header>
              <h2 id={`${top.id}-title`}>{top.title}</h2>
              <button aria-label="Close" onClick={() => hide(top.id)}>×</button>
            </header>
            <div>{top.body}</div>
            <footer>
              {top.secondary && <button onClick={top.secondary.onClick}>{top.secondary.label}</button>}
              {top.primary && <button onClick={top.primary.onClick}>{top.primary.label}</button>}
            </footer>
          </div>
        </div>
      )}
    </ModalContext.Provider>
  );
}

export function useModal() {
  const ctx = useContext(ModalContext);
  if (!ctx) throw new Error("useModal must be inside ModalProvider");
  return ctx;
}

Edge Cases and Gotchas ​

  • Focus trapping: when a modal opens, first focusable element should receive focus; Tab must cycle inside.
  • Escape key should close the top modal only.
  • Clicking backdrop: clarify - does it dismiss or not? Defaults differ.
  • Concurrent show calls: batched state update must preserve priority ordering.

Follow-ups to Expect ​

  • "Portal the modal to document.body." Swap the JSX root with createPortal(..., document.body).
  • "Animate enter/exit." Use framer-motion or CSS @keyframes with transitionend.
  • "Undismissable modals (critical errors)." Add a dismissible: false flag; hide the close icon and ignore Escape.

What They're Testing ​

Managing a collection of pending UI, priority-based scheduling, accessibility (aria-modal, focus trap).


Voting Poll UI Component ​

Reported Frequency: Mid. Easier problem sometimes used for L4.

Problem ​

Render a poll with N options. Each option has a vote button, the current count, and a percentage bar. Bar fills proportionally. Users can vote once per option (clarify) or unlimited (clarify). Color the leading option differently.

Solution - React + TypeScript ​

tsx
import { useMemo, useState } from "react";

type Option = { id: string; label: string };
type Props = { options: Option[] };

export function Poll({ options }: Props) {
  const [votes, setVotes] = useState<Record<string, number>>(
    () => Object.fromEntries(options.map((o) => [o.id, 0])),
  );

  const total = useMemo(
    () => Object.values(votes).reduce((a, b) => a + b, 0),
    [votes],
  );

  const leaderId = useMemo(() => {
    let best: string | null = null;
    let max = -1;
    for (const [id, count] of Object.entries(votes)) {
      if (count > max) {
        max = count;
        best = id;
      }
    }
    return max > 0 ? best : null;
  }, [votes]);

  const vote = (id: string) =>
    setVotes((v) => ({ ...v, [id]: v[id] + 1 }));

  return (
    <div style={{ display: "grid", gap: 8 }}>
      {options.map((o) => {
        const count = votes[o.id];
        const pct = total === 0 ? 0 : (count / total) * 100;
        const leading = o.id === leaderId;
        return (
          <div key={o.id} style={{ display: "grid", gap: 4 }}>
            <div style={{ display: "flex", justifyContent: "space-between" }}>
              <button onClick={() => vote(o.id)}>{o.label}</button>
              <span>{count} ({pct.toFixed(0)}%)</span>
            </div>
            <div style={{ height: 8, background: "#eee", borderRadius: 4 }}>
              <div
                style={{
                  width: `${pct}%`,
                  height: "100%",
                  background: leading ? "#16a34a" : "#2563eb",
                  transition: "width 200ms ease",
                }}
              />
            </div>
          </div>
        );
      })}
    </div>
  );
}

Edge Cases and Gotchas ​

  • Avoid division by zero when total is 0.
  • Ties: leader calculation must pick a stable winner or none; decide up front.
  • Transition on width feels nice but disable on bulk updates to avoid jank.

Follow-ups to Expect ​

  • "Persist votes to backend." Add useMutation per vote, with optimistic update and rollback on failure.
  • "One vote per user." Track votedFor in state and disable other buttons.
  • "Live updates from other users." Subscribe to a WebSocket channel, merge into state.

What They're Testing ​

Derived state (percentages), conditional styling, and whether you reach for useMemo when it actually helps.


Jira-like Drag & Drop Kanban ​

Reported Frequency: Mid-high. Expect this if the role leans into Uber's internal tooling.

Problem ​

Columns with cards. Drag a card within a column to reorder; drag across columns to move. Use HTML5 DnD API (no external lib).

Solution - React + TypeScript ​

tsx
import { useState } from "react";

type Card = { id: string; title: string };
type Board = Record<string, Card[]>;

const initial: Board = {
  todo: [{ id: "1", title: "Write spec" }, { id: "2", title: "Review PR" }],
  doing: [{ id: "3", title: "Ship feature" }],
  done: [],
};

export function Kanban() {
  const [board, setBoard] = useState<Board>(initial);
  const [dragging, setDragging] = useState<{ col: string; id: string } | null>(null);

  const onDragStart = (col: string, id: string) => setDragging({ col, id });

  const onDrop = (toCol: string, toIndex: number) => {
    if (!dragging) return;
    const { col: fromCol, id } = dragging;

    setBoard((prev) => {
      const next: Board = { ...prev, [fromCol]: [...prev[fromCol]], [toCol]: [...prev[toCol]] };
      const fromIdx = next[fromCol].findIndex((c) => c.id === id);
      if (fromIdx === -1) return prev;
      const [card] = next[fromCol].splice(fromIdx, 1);

      // if moving within same column and fromIdx < toIndex, target shifted left
      let insertAt = toIndex;
      if (fromCol === toCol && fromIdx < toIndex) insertAt -= 1;
      next[toCol].splice(insertAt, 0, card);
      return next;
    });

    setDragging(null);
  };

  return (
    <div style={{ display: "flex", gap: 12 }}>
      {Object.entries(board).map(([col, cards]) => (
        <div
          key={col}
          style={{ flex: 1, background: "#f5f5f5", padding: 8, minHeight: 200 }}
          onDragOver={(e) => e.preventDefault()}
          onDrop={(e) => {
            e.preventDefault();
            onDrop(col, cards.length);
          }}
        >
          <h3>{col}</h3>
          {cards.map((c, i) => (
            <div
              key={c.id}
              draggable
              onDragStart={() => onDragStart(col, c.id)}
              onDragOver={(e) => {
                e.preventDefault();
                e.stopPropagation();
              }}
              onDrop={(e) => {
                e.preventDefault();
                e.stopPropagation();
                onDrop(col, i);
              }}
              style={{
                padding: 8,
                margin: "4px 0",
                background: "white",
                border: "1px solid #ccc",
                cursor: "grab",
              }}
            >
              {c.title}
            </div>
          ))}
        </div>
      ))}
    </div>
  );
}

Edge Cases and Gotchas ​

  • dragover handler must call preventDefault() or drop will never fire.
  • Index shift when moving within the same column past the original position - easy off-by-one.
  • Mobile browsers don't emit HTML5 DnD events; for production you'd use Pointer Events.

Follow-ups to Expect ​

  • "Persist order to backend with optimistic UI." Dispatch mutation; revert on 5xx.
  • "Accessibility for non-mouse users." Arrow keys to pick and drop.
  • "Drop indicators (visual line where the card will land)." Track hover-index, render a spacer.

What They're Testing ​

DOM events depth, array immutability, clean index math.


Debounce / Throttle Polyfills ​

Reported Frequency: Very high. Expect at least one in R3.

Solution - TypeScript ​

typescript
type Fn = (...args: any[]) => any;

export function debounce<T extends Fn>(
  fn: T,
  wait: number,
  opts: { leading?: boolean; trailing?: boolean } = { trailing: true },
) {
  let timer: ReturnType<typeof setTimeout> | null = null;
  let lastArgs: Parameters<T> | null = null;
  let leadingCalled = false;

  function invoke() {
    if (lastArgs) {
      fn(...lastArgs);
      lastArgs = null;
    }
  }

  const debounced = (...args: Parameters<T>) => {
    lastArgs = args;
    if (opts.leading && !timer && !leadingCalled) {
      fn(...args);
      leadingCalled = true;
      lastArgs = null;
    }
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      if (opts.trailing) invoke();
      timer = null;
      leadingCalled = false;
    }, wait);
  };

  debounced.cancel = () => {
    if (timer) clearTimeout(timer);
    timer = null;
    lastArgs = null;
    leadingCalled = false;
  };

  return debounced;
}

export function throttle<T extends Fn>(
  fn: T,
  wait: number,
  opts: { leading?: boolean; trailing?: boolean } = { leading: true, trailing: true },
) {
  let lastCall = 0;
  let timer: ReturnType<typeof setTimeout> | null = null;
  let lastArgs: Parameters<T> | null = null;

  const throttled = (...args: Parameters<T>) => {
    const now = Date.now();
    const remaining = wait - (now - lastCall);
    lastArgs = args;

    if (remaining <= 0) {
      if (opts.leading !== false) {
        lastCall = now;
        fn(...args);
        lastArgs = null;
      } else {
        lastCall = now;
      }
    } else if (!timer && opts.trailing !== false) {
      timer = setTimeout(() => {
        lastCall = opts.leading === false ? 0 : Date.now();
        timer = null;
        if (lastArgs) {
          fn(...lastArgs);
          lastArgs = null;
        }
      }, remaining);
    }
  };

  throttled.cancel = () => {
    if (timer) clearTimeout(timer);
    timer = null;
    lastArgs = null;
    lastCall = 0;
  };

  return throttled;
}

Edge Cases and Gotchas ​

  • Trailing without leading: the common "only fire at end" case.
  • Leading without trailing: the "fire immediately then ignore for N ms" case.
  • Cancel must clear both the pending timer and any stored args.
  • If fn expects this, use fn.apply(thisArg, args) - but the question rarely requires it.

What They're Testing ​

Your understanding of time-based scheduling primitives.


Promise.all / Promise.any / Promise.race Polyfills ​

Reported Frequency: Very high.

Solution - TypeScript ​

typescript
export function promiseAll<T>(promises: Iterable<T | Promise<T>>): Promise<T[]> {
  return new Promise((resolve, reject) => {
    const arr = Array.from(promises);
    if (arr.length === 0) return resolve([]);

    const results: T[] = new Array(arr.length);
    let remaining = arr.length;

    arr.forEach((p, i) => {
      Promise.resolve(p).then(
        (value) => {
          results[i] = value;
          remaining -= 1;
          if (remaining === 0) resolve(results);
        },
        reject, // first rejection wins
      );
    });
  });
}

export function promiseRace<T>(promises: Iterable<T | Promise<T>>): Promise<T> {
  return new Promise((resolve, reject) => {
    for (const p of promises) {
      Promise.resolve(p).then(resolve, reject);
    }
  });
}

export function promiseAny<T>(promises: Iterable<T | Promise<T>>): Promise<T> {
  return new Promise((resolve, reject) => {
    const arr = Array.from(promises);
    if (arr.length === 0) {
      return reject(new AggregateError([], "All promises were rejected"));
    }
    const errors: any[] = new Array(arr.length);
    let remaining = arr.length;

    arr.forEach((p, i) => {
      Promise.resolve(p).then(resolve, (err) => {
        errors[i] = err;
        remaining -= 1;
        if (remaining === 0) reject(new AggregateError(errors, "All promises were rejected"));
      });
    });
  });
}

export function promiseAllSettled<T>(
  promises: Iterable<T | Promise<T>>,
): Promise<Array<{ status: "fulfilled"; value: T } | { status: "rejected"; reason: any }>> {
  return promiseAll(
    Array.from(promises).map((p) =>
      Promise.resolve(p).then(
        (value) => ({ status: "fulfilled" as const, value }),
        (reason) => ({ status: "rejected" as const, reason }),
      ),
    ),
  );
}

Edge Cases and Gotchas ​

  • Empty iterable: all resolves to [], any rejects with AggregateError, race pends forever.
  • Non-promise values must be wrapped with Promise.resolve so .then works.
  • Results array must be indexed by original order even though settlement order is arbitrary.

What They're Testing ​

Understanding the primitives you use every day.


Todo List with MCQs ​

Reported Frequency: Mid. Often the warmup round.

Problem ​

Build a todo app: add, delete, filter by all / active / done. After the coding part, expect 3-5 MCQs on React hooks behavior and CSS quirks.

Solution - React + TypeScript ​

tsx
import { useState } from "react";

type Todo = { id: string; text: string; done: boolean };
type Filter = "all" | "active" | "done";

export function TodoApp() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [input, setInput] = useState("");
  const [filter, setFilter] = useState<Filter>("all");

  const add = () => {
    const text = input.trim();
    if (!text) return;
    setTodos((t) => [...t, { id: crypto.randomUUID(), text, done: false }]);
    setInput("");
  };

  const toggle = (id: string) =>
    setTodos((t) => t.map((x) => (x.id === id ? { ...x, done: !x.done } : x)));

  const remove = (id: string) => setTodos((t) => t.filter((x) => x.id !== id));

  const visible = todos.filter((t) =>
    filter === "all" ? true : filter === "done" ? t.done : !t.done,
  );

  return (
    <div>
      <input
        value={input}
        onChange={(e) => setInput(e.target.value)}
        onKeyDown={(e) => e.key === "Enter" && add()}
      />
      <button onClick={add}>Add</button>

      <div>
        {(["all", "active", "done"] as Filter[]).map((f) => (
          <button key={f} onClick={() => setFilter(f)} aria-pressed={filter === f}>
            {f}
          </button>
        ))}
      </div>

      <ul>
        {visible.map((t) => (
          <li key={t.id}>
            <input type="checkbox" checked={t.done} onChange={() => toggle(t.id)} />
            <span style={{ textDecoration: t.done ? "line-through" : "none" }}>{t.text}</span>
            <button onClick={() => remove(t.id)}>x</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

Sample MCQs You May Hit ​

  • "Which of these triggers a re-render?" - setState(prev) with the same value triggers reconciliation but may bail out via Object.is; mutating state via state.push does not trigger re-render.
  • "What does useEffect(() => {...}, []) do?" - Run after mount, cleanup on unmount.
  • "Why does display: flex collapse margins differently than display: block?" - Flex containers don't collapse margins.
  • "What is the order of useEffect vs useLayoutEffect?" - useLayoutEffect fires synchronously after DOM mutations, before paint. useEffect fires asynchronously after paint.

What They're Testing ​

Basic React + JS fluency plus theory recall. Don't overbuild.


Final Reminders ​

  • Start talking before you start typing.
  • Extract utilities even if the problem seems small. It signals layering.
  • Always show one working thing in the first 15 minutes.
  • Leave 5 minutes at the end to walk through edge cases verbally.

Frontend interview preparation reference.