Prompt and scope
A dashboard card changes layout based on its own container rather than the viewport. The first implementation synchronously reads and writes styles inside a ResizeObserver callback, causing slow frames and occasional loop errors. Design observation, measurement, update, and cleanup. The core skills are browser layout and component lifecycles, so this belongs to frontend.
What the interviewer evaluates
The answer should distinguish viewport media queries from element observation and cover callback timing, box choices, read/write separation, loop protection, batched updates, React Strict Mode, multiple targets, hidden elements, SSR, and accessibility.
Questions to clarify first
- Do we observe the content box, border box, or device-pixel content box?
- Does the update change classes, CSS variables, Canvas dimensions, or DOM geometry?
- Will the callback change the observed element or an ancestor’s size?
- How many targets exist, and should updates be coalesced into the next frame?
- How are observer creation and cleanup handled on unmount, hidden state, and SSR?
30-second answer framework
“Create one stable observer on the client and observe the required box. In the callback, collect sizes first, then separate DOM reads from writes. Apply a class or CSS variable in a batched frame so changing layout cannot immediately feed back into the observed target. Ignore unchanged values; on unmount stop updates, unobserve, and disconnect. Test long tasks, layout count, loop errors, and Strict Mode remounts.”
Step-by-step solution
ResizeObserver watches an element’s content or border box, independent of the viewport. Choose the box deliberately: layout breakpoints usually use content or border size, while pixel-accurate drawing may use device-pixel content. Do not repeatedly read layout-forcing properties inside the callback.
First collect every entry’s latest size into a small pending set, then compute breakpoints together. Keep geometry reads in one batch and write CSS variables or classes after calculation. If the write can affect layout, schedule it with requestAnimationFrame and compare the next batch with the previous value.
Feedback loops are “observe size, write style, change size again.” For example, width changes padding and padding changes width. Add hysteresis, fixed container constraints, or update properties that do not alter the measured target. If resizing is required, cap work to one update per frame and detect convergence. The browser may defer non-converging notifications and report a loop error; ignoring the error is not a fix.
In React, create the observer in a client-only useLayoutEffect or suitable effect, with a stable ref and callback. Strict Mode can run setup and cleanup twice, so cleanup must be idempotent. On unmount stop applying updates, unobserve targets, and disconnect. SSR must not access window or construct the observer.
For many cards, share an observer and map entry.target to component state, but keep ownership clear so one card cannot block the whole callback. During a drag, keep only the latest size and merge updates in an animation frame rather than adding an arbitrary debounce that makes layout lag.
Hidden elements, display:none, font loading, and scrollbars can change size. Provide a usable default for an initial zero-size entry and switch after the first valid measurement. Prefer CSS container queries for pure style breakpoints; use ResizeObserver when JavaScript must drive Canvas, third-party widgets, or business calculations.
Verify with the Performance panel while dragging, loading fonts, hiding and showing cards, rotating the viewport, and mounting many instances. Assert that each size change causes only necessary work, with no persistent loop, duplicate observer, post-unmount update, or significant layout thrash. Test keyboard and screen-reader operation too.
Model high-quality answer
“I create a stable client-side ResizeObserver and choose content or border box intentionally. The callback records the latest entries and computes breakpoints; it does not repeatedly read and write layout. I apply CSS variables or classes in a batched frame. If a write changes the observed size, I add hysteresis, constraints, and convergence checks to prevent a feedback loop.
React cleanup is idempotent: stop updates on unmount, unobserve, and disconnect. Many cards can share an observer with target-based dispatch. I test drag resize, font loading, hidden and shown states, and Strict Mode remounts, measuring layouts, long tasks, loop errors, duplicate subscriptions, and accessible interaction.”
Common mistakes
- Treat ResizeObserver as viewport media query → components fail in different containers → observe the element itself.
- Read and write lots of DOM in the callback → forced layout and thrash → batch reads and write on the next frame.
- Change the observed target without convergence → feedback loop → use hysteresis, constraints, and detection.
- Create an observer on every render → duplicate callbacks and leaks → stable refs, dependencies, and cleanup.
- Only unobserve but keep callback state → updates continue after unmount → mark inactive, unobserve, and disconnect.
- Apply the largest layout to zero size → first paint jumps → use a default state until a valid entry.
- Use JavaScript for every breakpoint → needless complexity → use container queries when CSS is sufficient.
- Test only normal dragging → font, hidden, and Strict Mode issues escape → cover lifecycle and resource changes.
Follow-up questions and responses
Follow-up 1: When does the callback run?
The browser batches size-change notifications after layout. Non-converging changes may be deferred and raise a loop error, so the callback must not cause unbounded layout changes.
Follow-up 2: Why not use window.resize?
It reflects viewport changes, not a card changing because of a grid, sidebar, or font. ResizeObserver observes the element directly.
Follow-up 3: How do you separate reads and writes?
Read sizes from entries first, compute results, then batch CSS variable or class writes, using requestAnimationFrame when needed and ignoring unchanged values.
Follow-up 4: One observer per element or a shared observer?
A few independently owned elements can use separate observers. Many targets can share one and dispatch by target, as long as callback work remains bounded.
Follow-up 5: What happens after display:none?
Treat zero size as temporary, wait for a valid entry after showing, and avoid writing a loop while hidden. Do not treat zero as a permanent breakpoint.
Follow-up 6: Why does React Strict Mode expose bugs?
Development mode may run effect setup and cleanup twice to reveal asymmetric side effects. Cleanup must be repeatable and leave no observer or update callback behind.
Follow-up 7: When should CSS container queries win?
Use CSS for pure container breakpoints. Use ResizeObserver when size must drive Canvas, a third-party API, complex measurement, or business calculations.