skip to content

When you call observe(el) on a ResizeObserver, when does the callback first run, and what do unobserve() and disconnect() each do when the code that created the observer is torn down?

level: middleimportance: must knowfreq 58%

answer

  1. it fires before anything changed
  2. free initial measurement
  3. one target versus all targets
  4. cleanup is yours, not the collector's

basics

~20 s

Observing delivers an initial callback at the next rendering opportunity with the element's current size, even though nothing changed. unobserve(target) stops watching one element; disconnect() stops watching all of them and is what teardown should call, since the observer stays live otherwise.

solid answer

~40 s

`observe(el)` does not wait for a change: the element becomes an active observation immediately, so at the next rendering opportunity the callback runs once with its current size. That is useful — it gives you the initial measurement for free — but it means the callback must not assume a real resize happened. Calling `observe()` again on the same target replaces the existing observation rather than adding a second one. For teardown, `unobserve(target)` removes one element while the observer keeps watching the rest; `disconnect()` clears every observation at once and is what a component's cleanup should call. Don't leave it to garbage collection: an observer that is still observing still runs your callback, potentially for elements that are no longer on screen and against state that no longer exists.

code

javascript · 11 lines
javascript
function watchSize(el, onSize) {
  const ro = new ResizeObserver((entries) => {
    for (const entry of entries) onSize(entry.contentBoxSize[0].inlineSize);
  });
  ro.observe(el);
  return () => ro.disconnect();
}

const box = document.body.appendChild(document.createElement('div'));
const stop = watchSize(box, (width) => console.log('width', width));
// later: stop();

go deeper

for a junior

Remember the three methods and that cleanup is explicit: observe to start, unobserve for one element, disconnect for all. Say plainly that the callback fires once at the start with the current size.

for a middle

Explain that the initial delivery is asynchronous — next rendering update, not inside observe() — and that re-observing a target replaces rather than duplicates the observation. Know why the callback must loop over entries.

for a senior

Demonstrate teardown discipline: subscriptions that return their own cleanup, unobserve for shared observers versus disconnect for private ones, and callbacks written to survive a zero-size or post-removal delivery.

for a principal

Set the codebase convention so nobody hand-pairs observe and disconnect: one small subscription helper, one place that guards against zero sizes, and a review rule that a bare new ResizeObserver in feature code is a smell.

## observe() starts a subscription, not a one-shot read `ResizeObserver` has exactly three methods: `observe(target, options)`, `unobserve(target)` and `disconnect()`. Constructing the observer does nothing on its own; the first `observe()` call is what puts it to work. ```javascript const ro = new ResizeObserver((entries) => { /* ... */ }); ro.observe(el); ``` From that moment the element is in the observer's list of observation targets, and stays there until you remove it. ## The initial notification The behaviour that surprises people: **the callback fires once shortly after `observe()`, before anything has resized.** When observation begins on an element that is being rendered, that element counts as having an active observation, so the next time the browser runs its rendering update it delivers an entry with the element's current size. This is deliberate and helpful — you get the starting measurement without a separate `getBoundingClientRect()` call, and code that reacts to size does not have to special-case "the first time". But two consequences follow. First, the callback runs asynchronously, on the next frame, not synchronously inside `observe()` — so you cannot read the size on the very next line. Second, any logic of the shape "the user just resized me, animate the change" will run spuriously on mount unless you guard it. An element that is not rendered — `display: none`, or not in the document — generates no box, so there is nothing meaningful to report; write callbacks that tolerate zero sizes rather than treating every delivery as a real layout. ## What arrives afterwards After the initial delivery, entries arrive only for targets whose observed box actually changed. Delivery is batched: one callback invocation per observer per frame, carrying an array of the changed entries. Ten observed elements that all change in the same frame produce one callback with ten entries, which is why looping over `entries` — rather than assuming `entries[0]` is the one you care about — matters as soon as an observer watches more than one target. ## Re-observing the same target Calling `observe()` twice on the same element does not create two observations. The existing observation for that target is replaced, which is how you change the `box` option for an already-observed element. It also means an accidental repeat in a re-run effect is not a leak of observations — but it does trigger a fresh initial notification, which can look like a spurious resize. ## unobserve versus disconnect - `ro.unobserve(el)` removes one target. Everything else the observer watches continues. Use it when one item disappears from a list that a shared observer is watching. - `ro.disconnect()` removes all targets at once. The observer object survives and can be reused — calling `observe()` on it later starts fresh, including a new initial notification. `disconnect()` is not a destructor; it is "stop watching everything". Both are idempotent in practice: unobserving something you are not observing does nothing. ## Why teardown is your job The reliable rule is: whatever created the observer owns disconnecting it, in the same scope, on every exit path. Do not reason about whether the browser can collect a detached node while an observer references it — that is not a contract you can build on. What you *can* observe is the failure mode: an observer that is still observing still runs your callback, and that callback typically closes over things that are gone — a removed DOM node it wants to write to, a store that has been reset, a component instance that no longer exists. In a single-page app that navigates many times, forgotten observers accumulate one per visit, each doing a little work every frame in which its target changes. The pattern that avoids the mistake is to make the subscription return its own cleanup, so no caller has to remember two symmetrical calls: ```javascript function watchSize(el, onSize) { const ro = new ResizeObserver(([entry]) => onSize(entry.contentBoxSize[0].inlineSize)); ro.observe(el); return () => ro.disconnect(); } ``` When an observer is private to one element, `disconnect()` and `unobserve(el)` are equivalent, and `disconnect()` is the safer habit because it cannot be left holding a target you forgot about. When the observer is shared across many elements, `unobserve()` per element is the correct call and `disconnect()` would silently break every other subscriber.

  • Can you read the element's size on the line right after calling observe()?
    Not from the observer. The initial notification is delivered during the browser's next rendering update, so the callback has not run yet when the next statement executes. If you need the size synchronously at that instant you have to read layout yourself with `getBoundingClientRect()`, and accept the forced measurement that entails.
  • After calling disconnect(), can the same observer be used again?
    Yes. `disconnect()` clears the observation targets; it does not invalidate the object. Calling `observe()` on it afterwards starts a fresh observation, including a new initial notification. That is why `disconnect()` should be read as "stop watching everything" rather than "destroy this observer".
  • A shared observer watches every row of a list. One row is removed from the DOM. What should happen?
    Call `unobserve(row)` as part of removing it, and drop any registry entry keyed by that element. `disconnect()` would be wrong — it would stop every other row too. Leaving it observed is also wrong: the removed node stays referenced by the observer and by your registry, and the callback can still fire for it with zero sizes.

saying these in an interview costs you the question

  • Assumes the callback only runs after a genuine size change
  • Relies on garbage collection instead of calling disconnect()
  • Thinks unobserve() stops observation of every target
  • Believes disconnect() permanently destroys the observer object
  • Expects observe() to deliver the size synchronously

context