skip to content

Why should a component instance register the release of everything it acquires with its own scope, rather than at the site that removes it?

level: middleimportance: should knowfreq 58%

answer

  1. removal is not the only exit
  2. register beside the acquisition
  3. the remover cannot know what was acquired
  4. one registration, every exit path
  5. replacement is also a teardown

basics

~20 s

Because an instance can leave several ways - removed, replaced at the same position, carried off with an ancestor, discarded mid-update - and only the instance knows what it acquired. Registration makes cleanup run on every path.

solid answer

~50 s

Teardown is a contract, not a habit. The site that removes a component knows nothing about the timer, subscription, observer, listener or in-flight request that instance happened to acquire, and removal is not even the only exit: the position can be replaced by a different definition, an ancestor can be removed and take the whole subtree, or the runtime can abandon an instance it started building before any output was applied. If each acquisition registers its release at the moment of acquisition, with the scope that owns the instance, then the runtime runs those releases on whichever exit actually happens, in one place, with the handle still in scope. The pairing is also reviewable: an acquisition with no registered release is visible locally, while cleanup written at a removal site silently rots as soon as someone adds another way to remove the component.

go deeper

for a junior

Whatever a component starts, register how it stops in the same place. Timers, subscriptions and listeners do not stop by themselves when the component disappears from the screen.

for a middle

List the ways an instance can leave the tree and explain why only the instance knows what it acquired, so the release has to be registered beside the acquisition.

for a senior

Audit the exits rather than the happy path: replacement at the same position, an ancestor removal, an abandoned build. Check each release is safe to run early, once, or on a half-built instance.

for a principal

Make the pairing a reviewable convention and give shared code one acquire-and-register helper, so cleanup correctness stops depending on every author remembering every exit path.

"Clean up when the component is removed" sounds like a single case. It is not. An instance can stop existing several ways, and only one of them looks like the code that removed it. ## The paths out 1. **The parent stops including it.** A condition flips, a list loses an entry, a step is replaced by the next step. 2. **The position is filled by something else.** The same slot in the tree now holds a different definition, so the old instance is torn down and a new one is created rather than updated. Which changes make the runtime replace rather than update is the reconciler's decision and belongs to the identity and keys topic; what matters here is that replacement is a teardown. 3. **An ancestor is removed.** A whole subtree goes at once, and instances deep inside it are torn down without any code near them running. 4. **The tree itself is discarded.** A screen is unmounted, a region is swapped out, the application shuts a part of itself down. 5. **Work in progress is abandoned.** A runtime that can start building an instance and then throw that work away may create an instance whose output is never applied. Cleanup written at the site that removed the component covers case 1 only, and only for the removal sites that exist today. ## Why registration wins - **Locality.** The release sits next to the acquisition, with the same handle in scope, so the pair can be read as one unit and reviewed as one unit. - **Completeness.** The scope that owns the instance is notified on every exit, so one registration covers all the paths above. - **Ignorance is fine.** A parent does not need to know what its children acquired, which is what makes a component reusable in a tree the author never saw. - **Composition.** A reusable unit of stateful logic can acquire and register internally, and a component that uses three of them inherits three correct releases without writing any. - **Symmetry under replacement.** When a position is replaced, the old instance's registrations fire and the new instance registers its own, with no window where both or neither are live. ## What the scope guarantees, and what it does not | the scope does guarantee | the scope does not guarantee | |---|---| | every registered release is invoked when the instance is torn down | that a release you never registered is found and run for you | | it is invoked on whichever exit path occurred | a specific order across a subtree, which differs between runtimes | | it is invoked once per registration | that an asynchronous release has finished before the instance is gone | | the values captured at registration are still reachable | that the instance's state is still meaningful to read inside the release | Two consequences follow. First, a release must be safe to run when the work it cancels never started or has already finished, because an instance can be torn down at any moment - including between scheduling something and its running. Second, a release that returns before it is done, such as one that awaits a network acknowledgement, may be cut off; write it so that an unfinished release leaves nothing broken and never assumes the instance is still alive. ## The abandoned-instance case The fifth path is the one people miss. If a runtime may begin creating an instance and then discard that work - because a newer update superseded it, or a higher-priority change arrived - then an acquisition performed while output was being produced has no owner: the instance never reached the host, and there may be no teardown to pair with it. This is the sharpest reason acquisitions belong in the phase the runtime promises to pair with teardown rather than inline in output production. Put them where the promise holds, and the abandoned case is handled by the same registration as the other four. ## Reviewing for the pairing Read a component looking for verbs that start something, and ask what stops it: - a timer or a repeating loop, against a cancellation; - a subscription or a listener on a shared target, against a removal that passes the same reference; - an observer of size, visibility or mutation, against a disconnect; - an in-flight request, against a cancellation signal such as an abort controller; - an external widget or non-component library instance, against its own destroy step. Each of those pairs should be visible in one place. If finding the release means searching the parent, the screen, or a router hook, the contract has already been broken - not because that code is wrong today, but because the next exit path added will not go through it.

  • What happens to an instance the runtime starts building and then discards before any output is applied?
    It may never reach the host and may have no teardown paired with it, so anything acquired while output was being produced can be orphaned. That is why acquisitions belong in the phase the runtime promises to pair with teardown; registered there, the discard is handled like any other exit.
  • Does registering a release guarantee that it finishes?
    It guarantees the release is invoked on teardown, not that it completes. An asynchronous release can still be in flight when the instance is gone, and runtimes typically do not wait for it. Write releases so that an unfinished one leaves nothing inconsistent and never assumes the instance still exists.
  • Can a release rely on the teardown order within a subtree?
    Not portably. Runtimes differ over whether a subtree is released from the leaves upward or from the top down, and over where the release of a shared ambient value falls. A release should therefore tolerate a collaborator that is already gone rather than depend on being first.

saying these in an interview costs you the question

  • Writes cleanup only for the explicit removal path
  • Expects the parent to know what the child acquired
  • Assumes the runtime releases author-acquired resources for you
  • Skips release for work started before the first output
  • Depends on a fixed teardown order across a subtree