Skip to content

JS and React Fundamentals (Uber R2/R3) ​

Uber frequently drops fundamentals questions into R3 between coding problems, and sometimes saves a pure-theory chunk for R2. L5A means you're expected to answer these without hesitation and go one layer deeper than L4 would. This file is the complete cheat sheet.


Section 1: Event Loop ​

The pieces ​

JavaScript runs on a single thread. The event loop coordinates four main things:

  • Call stack — the execution stack of function frames.
  • Task queue (macrotask queue) — setTimeout, setInterval, I/O callbacks, UI events.
  • Microtask queue — Promise.then, queueMicrotask, MutationObserver.
  • Render pipeline — requestAnimationFrame callbacks, style recalc, layout, paint.

The loop's actual order (simplified) ​

  1. Run the oldest macrotask to completion.
  2. Drain the microtask queue completely (new microtasks added during draining also run).
  3. If a render is needed (roughly every 16.7ms), run requestAnimationFrame callbacks, then style/layout/paint.
  4. Back to step 1.

Why Promise.then fires before setTimeout(0) ​

typescript
setTimeout(() => console.log("timeout"), 0);
Promise.resolve().then(() => console.log("promise"));
console.log("sync");

// Output: sync → promise → timeout

The current synchronous script is itself a macrotask. When it finishes, the microtask queue drains before the next macrotask. Promise.then schedules a microtask; setTimeout schedules a macrotask. Microtasks always win within the same turn.

queueMicrotask ​

Manually schedule a microtask. Useful when you want something to happen before the next render but after the current stack clears.

typescript
queueMicrotask(() => {
  // runs after current synchronous code, before any setTimeout
});

requestAnimationFrame ​

Scheduled before the next paint. Callbacks get a high-resolution timestamp argument. Use this for visual animations so the browser can batch DOM reads and writes.

Tricky interview trace ​

typescript
console.log("A");

setTimeout(() => console.log("B"), 0);

Promise.resolve().then(() => {
  console.log("C");
  setTimeout(() => console.log("D"), 0);
});

queueMicrotask(() => console.log("E"));

console.log("F");

// Output: A → F → C → E → B → D

Walkthrough:

  • A and F are synchronous.
  • After sync, drain microtasks: C runs, which also queues D as a macrotask and enqueues no new microtask for E because E was already queued before C's then. Order of microtasks is FIFO: first-added first. The .then callback was added before queueMicrotask in the call order, so C before E.
  • Then macrotasks: B (queued first), D (queued later).

When this matters in interviews ​

Uber loves event-loop traces because they separate people who have memorized hooks from people who understand the runtime. If you get a trace question, write out the three queues on a whiteboard and literally tick through each step.


Section 2: Promises and Async ​

Promise states ​

pending → fulfilled (with a value) or rejected (with a reason). Once settled, never changes.

Chaining ​

typescript
fetchUser(id)
  .then((user) => fetchOrders(user.id)) // returning a promise chains it
  .then((orders) => render(orders))
  .catch((err) => showError(err))
  .finally(() => setLoading(false));
  • Returning a value from a .then wraps it in a fulfilled promise.
  • Returning a promise from a .then causes the chain to wait for it.
  • Throwing in a .then rejects the next link.
  • .finally gets no value and cannot change the chain's value.

async/await under the hood ​

async/await is syntactic sugar for generators + promises. The function is paused at each await and resumed when the awaited promise settles. The return value of an async function is always a promise.

typescript
async function x() {
  const user = await fetchUser();
  return user.name;
}

// roughly equivalent to:
function x() {
  return Promise.resolve()
    .then(() => fetchUser())
    .then((user) => user.name);
}

Promise.all / any / race / allSettled ​

MethodResolves whenRejects whenUse case
allAll resolveAny rejectsParallel work that all must succeed
anyAny resolvesAll reject"First one to succeed wins" (fastest replica)
raceFirst settles (either way)First settles if rejectionTimeout patterns
allSettledAll settleNeverYou need every outcome, success or failure

Implementations (from scratch) ​

typescript
export function promiseAll<T>(list: Array<Promise<T> | T>): Promise<T[]> {
  return new Promise((resolve, reject) => {
    if (list.length === 0) return resolve([]);
    const results: T[] = new Array(list.length);
    let remaining = list.length;
    list.forEach((p, i) => {
      Promise.resolve(p).then(
        (v) => {
          results[i] = v;
          if (--remaining === 0) resolve(results);
        },
        reject,
      );
    });
  });
}

export function promiseAny<T>(list: Array<Promise<T> | T>): Promise<T> {
  return new Promise((resolve, reject) => {
    if (list.length === 0) {
      return reject(new AggregateError([], "All promises were rejected"));
    }
    const errors: any[] = new Array(list.length);
    let remaining = list.length;
    list.forEach((p, i) => {
      Promise.resolve(p).then(resolve, (e) => {
        errors[i] = e;
        if (--remaining === 0) reject(new AggregateError(errors, "All promises were rejected"));
      });
    });
  });
}

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

type Settled<T> =
  | { status: "fulfilled"; value: T }
  | { status: "rejected"; reason: any };

export function promiseAllSettled<T>(list: Array<Promise<T> | T>): Promise<Settled<T>[]> {
  return promiseAll(
    list.map((p) =>
      Promise.resolve(p).then(
        (value): Settled<T> => ({ status: "fulfilled", value }),
        (reason): Settled<T> => ({ status: "rejected", reason }),
      ),
    ),
  );
}

Pitfalls ​

  • Forgetting to await — const user = fetchUser(); gives you a Promise, not a user. TypeScript catches it; JS doesn't.
  • Unhandled rejections — A .then with no .catch and no final await leaks unhandled rejection warnings. Always terminate chains with .catch or await inside a try/catch.
  • Sequential vs parallel — for (const id of ids) await fetchOne(id) is sequential. Use Promise.all(ids.map(fetchOne)) for parallel.
  • .then inside .then — pyramid of doom. Just return the inner promise from the outer .then.

When this matters in interviews ​

Uber will ask you to trace what a chain resolves to, to implement Promise.all from scratch, and to rewrite a callback soup into async/await. Practice each until it's muscle memory.


Section 3: Closures ​

Lexical scoping ​

A function "remembers" the variables in scope where it was defined, not where it was called. That persistent reference is a closure.

typescript
function outer() {
  const secret = 42;
  return function inner() {
    return secret; // inner closes over `secret`
  };
}
const f = outer();
f(); // 42

Practical uses ​

Private state

typescript
function counter() {
  let n = 0;
  return {
    inc: () => ++n,
    get: () => n,
  };
}

once — run a function at most once

typescript
export function once<T extends (...args: any[]) => any>(fn: T): T {
  let called = false;
  let result: ReturnType<T>;
  return ((...args: Parameters<T>) => {
    if (called) return result;
    called = true;
    result = fn(...args);
    return result;
  }) as T;
}

curry — break an n-ary function into unary calls

typescript
export function curry<T extends (...args: any[]) => any>(fn: T) {
  return function curried(...args: any[]): any {
    if (args.length >= fn.length) return fn(...args);
    return (...rest: any[]) => curried(...args, ...rest);
  };
}

const add = (a: number, b: number, c: number) => a + b + c;
const c = curry(add);
c(1)(2)(3); // 6
c(1, 2)(3); // 6

Memoize with closure

typescript
export function memoize<A extends any[], R>(fn: (...args: A) => R): (...args: A) => R {
  const cache = new Map<string, R>();
  return (...args: A): R => {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key)!;
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
}

Factory functions — returning preconfigured behavior. debounce and throttle (below) are exactly this.

Common interview trap: the var in loop ​

typescript
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0); // 3, 3, 3
}

var is function-scoped; all closures share the same i. Fix: use let (block-scoped), or wrap in an IIFE.

When this matters in interviews ​

Closures show up in every JS round. Uber's favorite is "implement memoize" or "what does this loop print and why." Know both cold.


Section 4: this Binding ​

The four rules (in precedence order) ​

  1. new binding — new Foo() — this is the newly created object.
  2. Explicit binding — fn.call(obj), fn.apply(obj, args), fn.bind(obj).
  3. Implicit binding — obj.fn() — this is obj.
  4. Default binding — standalone fn() — this is undefined in strict mode, window otherwise.

Arrow functions ​

Arrow functions do not get their own this. They inherit from the enclosing lexical scope. call/apply/bind cannot change it.

typescript
const obj = {
  x: 10,
  regular() { return this.x; },       // 10 when called as obj.regular()
  arrow: () => this,                   // `this` is whatever `this` was when the object literal was evaluated (global, undefined, etc.)
};

call / apply / bind ​

  • call(thisArg, a, b) — invoke with this bound, args as list.
  • apply(thisArg, [a, b]) — invoke with this bound, args as array.
  • bind(thisArg, a) — return a new function with this and optional partial args bound.

bind polyfill ​

typescript
declare global {
  interface Function {
    myBind(this: Function, thisArg: any, ...bound: any[]): any;
  }
}

Function.prototype.myBind = function (thisArg: any, ...bound: any[]) {
  const fn = this as Function;

  function bound_(this: any, ...called: any[]) {
    // If called with `new`, ignore `thisArg` - use the newly created object
    const isNew = this instanceof bound_;
    return fn.apply(isNew ? this : thisArg, [...bound, ...called]);
  }

  // Preserve prototype chain so `instanceof` works for bound constructors
  if (fn.prototype) {
    bound_.prototype = Object.create(fn.prototype);
  }

  return bound_;
};

Call with:

typescript
function greet(this: { name: string }, greeting: string) {
  return `${greeting}, ${this.name}`;
}
const hi = greet.myBind({ name: "Aayush" }, "Hello");
hi(); // "Hello, Aayush"

Common pitfalls in React callbacks ​

  • Class component onClick={this.handleClick} loses this unless bound in the constructor or written as an arrow class field.
  • Passing array.forEach(this.doThing) in a class method — same problem.
  • Modern function components with hooks avoid this entirely because there is no this.

When this matters in interviews ​

Uber's R3 loves "what does this refer to here" puzzles. The bind polyfill is a standard ask - don't forget the new handling and the prototype chain line, because interviewers do check for both.


Section 5: Prototypes and Inheritance ​

The prototype chain ​

Every object has an internal [[Prototype]] (accessible as __proto__) pointing to another object. Property lookup walks up the chain.

typescript
const animal = { eats: true };
const rabbit = Object.create(animal);
rabbit.jumps = true;
rabbit.eats;  // true (inherited)
rabbit.jumps; // true (own)

Object.create vs class ​

class is syntactic sugar over prototype chains plus constructor functions. class Foo {} creates a constructor function whose prototype is an object the class methods live on. Instances have __proto__ === Foo.prototype.

typescript
class Dog {
  bark() { return "woof"; }
}
const d = new Dog();
Object.getPrototypeOf(d) === Dog.prototype; // true

Implement new from scratch ​

typescript
function myNew<T>(Constructor: new (...args: any[]) => T, ...args: any[]): T {
  // 1. Create a new object whose prototype is Constructor.prototype
  const obj = Object.create(Constructor.prototype);
  // 2. Invoke the constructor with `this` bound to the new object
  const result = (Constructor as any).apply(obj, args);
  // 3. If the constructor returned an object, return that; otherwise return obj
  return (result !== null && typeof result === "object") ? result : obj;
}

class Point {
  constructor(public x: number, public y: number) {}
  distance(): number { return Math.hypot(this.x, this.y); }
}

const p = myNew(Point, 3, 4);
p.distance(); // 5
p instanceof Point; // true

When this matters in interviews ​

instanceof checks walk the prototype chain. If you implement new you must set the prototype. If you bind a constructor, you must preserve the chain (see bind polyfill above). Prototype questions have been less common recently but still surface in L5A+ rounds.


Section 6: Debounce and Throttle ​

Definitions ​

  • Debounce — delay execution until the caller has been idle for N ms. Use for "fire only at the end" (search input, resize settling).
  • Throttle — execute at most once per N ms. Use for "fire at a steady rate" (scroll handlers, mouse-move).

When each fits ​

  • Search autocomplete → debounce 150-300ms.
  • Window resize settled → debounce.
  • Scroll position tracker → throttle.
  • Analytics pings during scroll → throttle.

Implementations ​

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 didLeading = false;

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

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

  debounced.flush = () => {
    if (timer && lastArgs) {
      clearTimeout(timer);
      fn(...lastArgs);
      timer = null;
      lastArgs = null;
    }
  };

  return debounced;
}

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

  const throttled = (...args: Parameters<T>) => {
    const now = Date.now();
    if (!last && opts.leading === false) last = now;
    const remaining = wait - (now - last);
    lastArgs = args;

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

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

  return throttled;
}

React hooks ​

typescript
import { useEffect, useMemo, useRef, useState } from "react";

export function useDebouncedValue<T>(value: T, wait: number): T {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const t = setTimeout(() => setDebounced(value), wait);
    return () => clearTimeout(t);
  }, [value, wait]);
  return debounced;
}

export function useThrottledCallback<T extends Fn>(fn: T, wait: number) {
  const fnRef = useRef(fn);
  fnRef.current = fn; // keep latest without re-creating the throttle
  return useMemo(
    () => throttle(((...args: any[]) => fnRef.current(...args)) as T, wait),
    [wait],
  );
}

When this matters in interviews ​

Debounce/throttle polyfills are asked in 80% of Uber machine coding rounds. Write them from memory, with cancel, and explain leading vs trailing edge clearly.


Section 7: React Hooks Deep Dive ​

useState ​

Lazy initial state: If the initial value is expensive, pass a function. It runs only on first render.

typescript
const [items, setItems] = useState(() => expensiveCompute());

Functional updates: When the next state depends on the previous, pass a function. Avoids stale closures.

typescript
setCount((c) => c + 1);

Batching: React 18 batches state updates across timeouts, promises, and native events. Multiple setState calls in a row produce one re-render.

useEffect ​

Dependency array:

  • undefined → runs after every render.
  • [] → runs once after mount; cleanup on unmount.
  • [a, b] → runs when a or b changes (by Object.is).

Cleanup runs before the next effect. If you set an interval and the deps change, cleanup runs first, then the new effect.

Race conditions in async effects:

typescript
useEffect(() => {
  let alive = true;
  fetchUser(id).then((u) => {
    if (alive) setUser(u);
  });
  return () => { alive = false; };
}, [id]);

Without the alive flag, a slow response for an old id can overwrite the latest user.

useMemo vs useCallback vs React.memo ​

HookMemoizesUse when
useMemoA valueThe computation is expensive OR the value is a dep of another hook
useCallbackA function referenceThe function is a prop to a memoized child OR a dep of another hook
React.memoA componentRe-renders are expensive AND props usually don't change

When each is NOT worth it:

  • useMemo(() => a + b, [a, b]) — the compute is trivial; the memo overhead exceeds the saving.
  • useCallback on a function used only by non-memoized children — useless.
  • React.memo on a cheap component with frequently changing props — actively slower because of the extra shallow compare.

useRef ​

Mutable container whose .current survives re-renders without triggering one. Use for:

  • DOM references (ref.current.focus()).
  • Instance-like mutable values (timers, previous values).
  • Stable callbacks when you don't want useCallback.

Custom hooks ​

A function whose name starts with use and calls other hooks. Rules:

  1. Only call hooks at the top level. No conditionals, loops, nested functions.
  2. Only call hooks from React functions or other custom hooks.

Composition example:

typescript
function useDebouncedSearch(query: string, wait = 200) {
  const debounced = useDebouncedValue(query, wait);
  const { data, loading } = useFetch(`/api/search?q=${encodeURIComponent(debounced)}`);
  return { results: data, loading };
}

useLayoutEffect vs useEffect ​

  • useEffect fires asynchronously after paint. User sees the update, then your effect runs.
  • useLayoutEffect fires synchronously after DOM mutation, before paint. User never sees the intermediate state.

Use useLayoutEffect when you need to measure DOM and adjust before paint (tooltip positioning). Otherwise use useEffect; it doesn't block paint.

useReducer ​

Better than useState when:

  • Multiple state fields that change together.
  • Next state depends on the previous in non-trivial ways.
  • You want logic testable outside the component (the reducer is a pure function).
typescript
type State = { count: number; history: number[] };
type Action = { type: "inc" } | { type: "dec" } | { type: "reset" };

function reducer(s: State, a: Action): State {
  switch (a.type) {
    case "inc": return { count: s.count + 1, history: [...s.history, s.count + 1] };
    case "dec": return { count: s.count - 1, history: [...s.history, s.count - 1] };
    case "reset": return { count: 0, history: [] };
  }
}

const [state, dispatch] = useReducer(reducer, { count: 0, history: [] });

When this matters in interviews ​

Expect "what's the difference between useMemo and useCallback" and "when is React.memo worth it." L5A candidates are expected to say "it's not always worth it" instead of blanket "use them always."


Section 8: React Rendering Model ​

Reconciliation and diffing ​

On each render, React creates a new virtual tree and diffs against the previous. For elements of the same type at the same position, React reuses the DOM node and updates props. For different types, React unmounts the old and mounts the new.

Keys ​

Keys tell React which item is which when reordering arrays. Bad key choice (array index) leads to input state leaking across items when the list reorders. Use stable, unique ids.

tsx
// Bad
items.map((item, i) => <Row key={i} item={item} />)

// Good
items.map((item) => <Row key={item.id} item={item} />)

Controlled vs uncontrolled ​

  • Controlled — React state is the source of truth; value is set from state, onChange updates state.
  • Uncontrolled — DOM is the source of truth; read via ref.current.value.

Controlled is default. Uncontrolled is fine for simple forms, large text inputs where every keystroke re-rendering is wasteful, or when integrating with non-React code.

Context API re-renders ​

Any component consuming a Context re-renders when the provider's value changes, regardless of which field of value they use. The common bug:

tsx
<Provider value={{ user, setUser }}>  // new object every render

Fixes:

  • Memoize the value: useMemo(() => ({ user, setUser }), [user]).
  • Split into multiple contexts (one for rarely-changing config, one for hot state).
  • Use a selector-based store (Zustand, Redux) for anything large.

Virtual DOM vs Real DOM ​

The virtual DOM is a JS object tree that mirrors the real DOM. React diffs virtual trees in memory, then applies only the changes to the real DOM. Real DOM operations are expensive (layout, paint); batching through the VDOM reduces them.

Fiber architecture ​

React 16+ rewrite. Fiber is a unit of work representing a component. Reconciliation is split into incremental work slices that can be paused and resumed. Enables Suspense, concurrent features, time-slicing (yield to the browser between long trees).

Just-enough-for-interview summary:

  • Old React (stack reconciler) walked the tree recursively; couldn't interrupt.
  • Fiber represents each component as a node in a linked list; work is cooperative; React can pause when the main thread has higher-priority work (animation, user input).

Strict Mode double-invoke ​

In development, Strict Mode intentionally runs effects, state updaters, and render twice to surface unintentional side effects. Production ignores Strict Mode. If your code breaks under Strict Mode, it's buggy - don't turn it off.

Suspense ​

Component for declarative loading states. Inside a <Suspense fallback={...}>, any component that throws a promise causes React to render the fallback until the promise resolves. Used by React.lazy, and by data-fetching libraries like Relay.

tsx
<Suspense fallback={<Spinner />}>
  <LazyProfile userId={id} />
</Suspense>

Error Boundaries ​

Class components (no hook equivalent yet) that catch errors in their child tree. static getDerivedStateFromError and componentDidCatch.

tsx
class ErrorBoundary extends React.Component<
  { fallback: React.ReactNode; children: React.ReactNode },
  { hasError: boolean }
> {
  state = { hasError: false };
  static getDerivedStateFromError() { return { hasError: true }; }
  componentDidCatch(err: Error) { log(err); }
  render() { return this.state.hasError ? this.props.fallback : this.props.children; }
}

When this matters in interviews ​

Be crisp on keys (this is the single most common "why is this bug happening" React question), Context re-renders (senior-level signal), and Fiber at the level of "incremental work and interruptibility." Uber R3 has been known to ask "why does my Context consumer re-render when I didn't change the value my component uses."


Section 9: Performance Optimization ​

Memoization: when NOT worth it ​

  • Cheap compute (simple arithmetic, short string ops).
  • Values that change every render anyway.
  • Components with frequently changing props.
  • Small trees where re-render cost is under 1ms.

The rule: measure before optimizing. Profile with the React DevTools Profiler; pick the top 3 longest renders.

Virtualization ​

Render only the rows in the viewport plus a small overscan buffer. Libraries: react-window (simpler), @tanstack/react-virtual (more flexible).

tsx
import { FixedSizeList } from "react-window";

<FixedSizeList
  height={500}
  itemCount={10000}
  itemSize={32}
  width="100%"
>
  {({ index, style }) => <div style={style}>Row {index}</div>}
</FixedSizeList>

Code splitting ​

React.lazy + Suspense splits at route or feature boundaries.

tsx
const Admin = React.lazy(() => import("./Admin"));

<Suspense fallback={<Spinner />}>
  <Admin />
</Suspense>

Webpack/Vite chunk the import; the chunk loads only when <Admin /> renders.

Bundle size reduction ​

  • Tree shaking (named imports from ES modules, no side-effect imports).
  • Dynamic imports for rarely used code.
  • Swap heavy libraries (moment → date-fns, lodash → lodash-es named imports).
  • Use @rollup/plugin-visualizer or rollup-plugin-analyzer to see what's big.

Image optimization ​

  • Lazy loading: <img loading="lazy" /> or IntersectionObserver.
  • Responsive: <img srcset="... 1x, ... 2x" sizes="..."> to serve correct size.
  • Modern formats: WebP for wide support, AVIF for cutting edge (fallback via <picture>).

Core Web Vitals ​

MetricTargetWhat frontend can do
LCP (Largest Contentful Paint)< 2.5sPreload hero image, optimize critical CSS, server-side render
INP (Interaction to Next Paint)< 200msAvoid long tasks, code-split, yield to main thread (scheduler.yield)
CLS (Cumulative Layout Shift)< 0.1Reserve space for images/ads, avoid inserting content above existing content

When this matters in interviews ​

System design will ask about bundle budgets and Core Web Vitals. Fundamentals rounds ask "when should you NOT use useMemo." Always answer with a cost/benefit frame.


Section 10: Browser and DOM ​

Event delegation ​

Attach one listener to a container; inspect event.target inside. Instead of N listeners, you have 1. Handles dynamically added children for free.

typescript
document.getElementById("list")!.addEventListener("click", (e) => {
  const li = (e.target as HTMLElement).closest("li");
  if (!li) return;
  handleClick(li.dataset.id!);
});

Bubbling vs capturing ​

Events travel in two phases:

  1. Capturing — from window down through ancestors to the target.
  2. Target — the element itself.
  3. Bubbling — from the target back up through ancestors.

addEventListener(type, handler) defaults to bubbling. Pass { capture: true } for the capturing phase. Use stopPropagation() to stop the current phase from continuing. Use stopImmediatePropagation() to also prevent other listeners on the same element.

IntersectionObserver ​

Declarative "is this element visible" API. Perfect for infinite scroll, lazy loading images, tracking ad impressions.

typescript
const observer = new IntersectionObserver((entries) => {
  for (const e of entries) {
    if (e.isIntersecting) loadMore();
  }
}, { rootMargin: "200px" });

observer.observe(sentinelEl);

rootMargin: "200px" triggers 200px before the element enters the viewport, so you prefetch in advance.

ResizeObserver ​

Fires when an element's size changes. Replaces hacks like listening to window.resize plus checking element size manually.

typescript
const ro = new ResizeObserver((entries) => {
  for (const e of entries) {
    console.log(e.contentRect.width);
  }
});
ro.observe(box);

MutationObserver ​

Fires when the DOM tree changes (children added/removed, attributes changed). Use for syncing external libraries with React-rendered DOM, or detecting third-party injections.

Layout thrashing ​

When JS forces the browser to recompute layout repeatedly in the same frame. Caused by read-write-read-write patterns:

typescript
// Bad: each read forces layout after the previous write
for (const el of elements) {
  el.style.width = el.offsetWidth + 10 + "px";
}

// Good: batch reads, then batch writes
const widths = elements.map((el) => el.offsetWidth);
elements.forEach((el, i) => {
  el.style.width = widths[i] + 10 + "px";
});

The cause: any layout-dependent read (offsetWidth, getBoundingClientRect, getComputedStyle, clientHeight) invalidated after a write forces synchronous layout.

requestAnimationFrame vs setTimeout ​

  • setTimeout(fn, 16) — "run after 16ms." Drifts, runs even if tab is hidden.
  • requestAnimationFrame(fn) — "run before next paint." Throttled to screen refresh rate. Paused when tab is hidden. Correct for visual animation.

When this matters in interviews ​

Event delegation is asked directly. IntersectionObserver for infinite scroll is expected in system design. Layout thrash ("how do you diagnose jank") is a senior-signal question - mention DevTools Performance panel, long tasks, forced reflow markers.


Closing Reminders ​

  • Event loop trace questions are the trickiest. Walk through queues literally.
  • Implementing polyfills is 60% of R3. Practice bind, debounce, throttle, promise combinators until automatic.
  • Know the cost side of every React optimization. "useMemo is not always free" is a senior answer.
  • When in doubt, say what you'd measure, then what you'd change. Interviewers love profiling-first thinking.

Frontend interview preparation reference.