skip to content

As a lead, how do you decide whether synchronization with an external system lives in a component's effects or in a long-lived scope outside the tree?

level: principalimportance: should knowfreq 44%

answer

  1. lifetime, not layering
  2. who is accountable for ending it
  3. framework disposes what an owner owns
  4. hoist only for sharing or survival
  5. no disposal path is a decision to leak

basics

~20 s

Match the sync to the lifetime of what needs it. A component-owned effect is the default: the runtime disposes it at teardown. A scope outside the tree buys sharing and longevity but must supply its own disposal path.

solid answer

~50 s

I treat it as a lifetime question, not a layering preference. A component-owned effect is the default: its scope is exactly the component, and the runtime calls its cleanup at teardown, so disposal is guaranteed without anyone remembering it. I hoist only for reasons that better cleanup cannot fix - several components need the same connection at once, or the sync must survive a component unmounting during navigation, or reconnecting is expensive against high mount churn. Hoisting means the framework stops disposing it for me, so the trade has a price: a named owner, an explicit disposal call on an explicit boundary, reference counting if the resource should exist only while someone wants it, and a live count in development so an unbalanced pair is visible. Work outside the tree also survives tests and can start where its host APIs do not exist.

go deeper

for a junior

Default to keeping side work inside the component that needs it, because the framework will clean it up for you. Moving it outside is a decision, not a simplification.

for a middle

Explain what changes when work leaves the component: no teardown calls its cleanup, so something explicit must. Be able to name what a scope releases when it is disposed.

for a senior

Decide from evidence - shared consumers, mount churn, reconnect cost - and ship the disposal path and the live counters alongside the hoist, not afterwards.

for a principal

Own the exchange: each step away from a component trades guaranteed cleanup for sharing or longevity. Write the rule, make hoisting a reviewed exception with a named owner, and say what ends it.

## The real question is lifetime "Where should this synchronization live?" is asking **how long must it exist, and who is accountable for ending it?** A component-owned effect answers both crisply: it lives as long as the component, and the runtime calls its cleanup at teardown. That is the right default, and the reason to leave it is when the thing being synchronized does not share a component's lifetime - a connection several views read from, an authenticated session, a background poll that must survive navigation between screens. ## The three homes and what each guarantees | Home | Lifetime | Who disposes | Main risk | |---|---|---|---| | Per-component effect | Exactly the component | The runtime, at teardown | Churn if the component mounts and unmounts often; duplicated work across sibling views | | A scope owned by a subtree or provider | As long as that subtree | The runtime, when the owner is torn down | Placed too high, it never ends in practice; placed too low, it is recreated | | A long-lived scope outside the tree | Until something explicitly ends it | Whatever code created it - i.e. you | Nothing calls cleanup, so it leaks past the UI that needed it and is invisible in review | The third row is the one that needs governance. An effect created outside any component has no owner supplied by the framework: it belongs to whatever scope created it, and disposing that scope is what releases its subscriptions and timers. If no scope exists, the effect exists forever, which is fine for exactly one case - something genuinely global - and wrong for everything else. ## What I ask before deciding 1. **Does more than one component need the same connection at once?** If yes, per-component effects duplicate it, and the duplication is not fixable with better cleanup. 2. **Must it survive a component unmounting?** Navigation away and back, a collapsed panel, a route change - if the sync must keep going, it cannot be owned by something that stops existing. 3. **How often does the owner mount and unmount?** High churn against an expensive external connection argues for hoisting; a cheap listener does not care. 4. **Can the work be resumed cheaply?** If reconnecting is free, keeping it component-owned is simpler and fails safer than keeping a long-lived resource alive. 5. **Who ends it, and can I see that in review?** A shared scope with no disposal path is a decision to leak. I would rather have a slightly redundant per-component effect than an unowned global one. 6. **What does it do to server-side rendering and tests?** Work outside the tree is not torn down by rendering a component and throwing it away, so it leaks between tests and can start on a server where its host APIs do not exist. ## Disposal accountability is the deliverable Hoisting synchronization is a trade: the framework stops calling cleanup for you, so you must supply the mechanism that does. Concretely, what I require before approving a long-lived scope: - **A named owner** - one module, one scope object, one lifecycle - with a documented start and stop. - **A disposal path that is actually called**, on sign-out, on tearing down the application shell, on whatever boundary genuinely bounds the work. - **Reference counting or nothing.** If the resource should exist only while at least one view wants it, count subscribers and close at zero. If that is too subtle for the team, keep it per component. - **An observable count in development** - live connections, live timers - so an unbalanced pair is visible rather than theoretical. - **A rule for restart.** A long-lived sync will fail at some point; who reopens it, and does a component notice? ## The rule I write down for a team Default to a component-owned effect. Hoist to a scope owned higher in the tree when several sibling components need the same external thing at the same time, and only then. Go outside the tree only for things that are genuinely application-wide and that have an explicit disposal call on an explicit boundary - and treat every such case as a reviewed exception with a named owner, not as a pattern to copy. The reasoning I want the team to internalise is not "global is bad": it is that the framework's one strong guarantee here is *cleanup paired with an owner's teardown*, and each step away from a component gives up part of that guarantee in exchange for sharing or longevity. Make the exchange deliberately, and pay for it with a disposal path you can point at.

  • What must be arranged for an effect created outside any component, and why is it easy to get wrong?
    It needs an owning scope that can be disposed, because no component teardown will ever release it. Whatever created the scope holds the subscriptions and timers until the scope is disposed. It is easy to get wrong because the code looks finished and works: nothing fails at the moment the disposal path is omitted, only later, and never in the file that omitted it.
  • A team proposes hoisting a connection out of components because cleanup bugs keep recurring. How do you respond?
    Recurring cleanup bugs are an argument for fixing the pairing, not for removing the only automatic disposal the framework offers. Hoisting moves the same bug somewhere with weaker guarantees. I would hoist only if the connection is genuinely shared or must outlive a component, and would then require the disposal path and instrumentation the recurring bugs prove the team needs.
  • How does work living outside the component tree complicate testing and server rendering?
    Rendering a component and discarding it no longer tears the work down, so timers and subscriptions leak between tests and make them order-dependent. On a server, module-level setup can run where the host APIs it assumes do not exist, or start per-request work that nothing ends. Both are arguments for keeping sync inside an owner the test or request already disposes.

saying these in an interview costs you the question

  • Hoists synchronization to module scope to avoid writing cleanup
  • Assumes something outside the tree is disposed when the UI unmounts
  • Treats a shared connection as free because only one object holds it
  • Counts subscribers nowhere but expects the resource to close itself
  • Judges the choice by code tidiness rather than by lifetime