skip to content

Two components call the same reusable stateful logic unit; what do they share, and what does each get its own copy of?

level: middleimportance: must knowfreq 72%

answer

  1. shares logic, not state
  2. one copy per call site
  3. cleanup belongs to the calling instance
  4. shared data needs a store or an ancestor

basics

~10 s

They share the code, not the data. Each call site gets its own state, derived values, effects and cleanup, owned by the calling component instance, so one caller's updates never reach the other's copy.

solid answer

~50 s

A reusable stateful unit is a plain function that packages state, derived values, effects and their cleanup, and is called inside a component's own reactive scope instead of being rendered as a component. Because it runs as part of that component's setup, everything it creates belongs to that instance: two components calling it get two independent copies, and when an instance is torn down the cleanup its unit registered runs with it. So a unit reuses *logic*, not *state*. If two components must see the same data, that data has to live somewhere both reach — state in a common ancestor, or an external store the unit reads. Units compose by calling other units, and in runtimes that key a unit's state by call order rather than by name, the call has to be unconditional.

code

pseudocode · 13 lines
pseudocode
# a reusable stateful unit: a plain function, called inside a component
function pollingUnit(url):
    state data = null
    effect(on: [url]):
        timer = every 5s: data = fetch(url)
        cleanup: stop(timer)      # released with the calling instance
    return data

component Dashboard:
    stats = pollingUnit("/stats")   # Dashboard's own state and timer

component Sidebar:
    stats = pollingUnit("/stats")   # a second, independent state and timer

go deeper

for a junior

Remember the one-line rule: calling the same reusable unit in two components gives you two separate copies of its state. It reuses code, not data.

for a middle

Explain why: the unit runs inside the calling component's scope, so its state and its cleanup are keyed to that instance. Then say where genuinely shared data has to live instead.

for a senior

Demonstrate the leak angle — anything a unit acquires must register its release, or it leaks once per caller — and know the call-order constraint and which reactivity models impose it.

for a principal

Decide policy: which behaviours become units, which become a shared store, and what prevents a codebase from ending up with both owning the same data under different names.

## What a reusable stateful unit is Not every kind of reuse is markup. Sometimes what repeats across components is **behaviour with state**: tracking whether an element is on screen, keeping a debounced copy of a value, subscribing to connection status, holding a form field's touched-and-error pair. Making that a component forces a tree level on every consumer and makes the state reachable only where that component renders. The alternative every modern component framework converged on is a **reusable stateful unit**: a plain function that packages state, derived values, effects and their cleanup, and is **called inside a component's own scope** rather than rendered. It is not a component — no markup, no place in the tree. It runs as part of the calling component's setup or render, and whatever it creates belongs to that component instance. ## Why it shares logic and not state - **State is created per call.** Each call executes the function again and creates fresh state, so two components calling it hold two unrelated copies. - **The owner is the caller, not the unit.** The unit is code; the instance that called it is where the state lives. Ten instances of one component mean ten copies. - **Updates do not cross.** A write lands in the copy owned by the component that made the call, and only that component, plus whatever it passes down, reacts. - **Teardown follows the owner.** Cleanup the unit registered runs when the calling instance is torn down, or before an effect re-runs where that effect has dependencies. This is worth saying out loud in an interview, because the opposite intuition is common: we imported the same thing, so we must see the same data. One definition, many instances — the same relationship a class has to its objects. ## Where the teardown belongs 1. The unit acquires something: a timer, a subscription, a listener, an observer, an in-flight request. 2. It registers the release **next to the acquisition**, as part of the same effect. 3. The framework runs that release when the calling instance goes away, or when the effect's inputs change and it is about to run again. A unit that acquires without registering a release leaks **once per caller**, and the leak grows with how successful the unit turned out to be. This is the most common defect in extracted logic, and the reason the acquire/release pair belongs inside the unit rather than in each consumer's own teardown. ## When you actually want shared data | Need | Reach for | Who owns the data | |---|---|---| | Same behaviour, independent data per component | a reusable stateful unit | each calling instance | | Same data for one subtree | state in a common ancestor, passed down or provided | the ancestor | | Same data across unrelated parts of the app | an external store the unit reads and writes | the store | A unit and a store are not rivals. The usual shape is a thin unit that subscribes to a store and exposes a slice of it, giving callers one ergonomic call site and one shared owner. The mistake is hoisting the unit's state into a module-level variable to get sharing: that silently couples every caller, including two instances on the same screen that wanted independence, and the state then outlives every component that used it. ## Composition and the call rule Units compose by **calling other units**. Because the whole chain runs inside the calling component's scope, an inner unit's state and cleanup still belong to that same instance. That transitivity is what makes small units worth writing: a visibility unit built on an element-observer unit needs no plumbing between them. It also means the runtime's constraints apply through the chain. Where a runtime keys a unit's state **by call order** rather than by name, every call must happen unconditionally and in the same order on every run: skipping one shifts every state slot after it, and the component starts reading a neighbour's value. Runtimes that key state by identity, or that run setup once per instance, do not have that rule — which is why the constraint sounds arbitrary until you know which model you are in. ## Designing the unit's surface - Return the smallest useful shape: the values and the actions, not the internals. - Take arguments rather than reading ambient values, so the unit stays callable from anywhere. - Keep one concern per unit; a unit that does two things cannot be reused for one of them. - Name what it owns rather than how it is implemented, so the storage can change without touching call sites. - Expose an explicit way to reset or re-key when callers need a fresh copy without re-mounting.

  • Two screens must react to the same data. Why does moving the unit's call higher not achieve that?
    Because each call still creates its own copy: calling it in a parent gives the parent a third copy, not a shared one. Sharing needs a single owner both readers reach — state lifted into a common ancestor and handed down, or an external store. The unit then wraps access to that owner instead of holding the state itself.
  • What must a unit do about work it starts, such as timers, subscriptions and listeners?
    Register the release alongside the acquisition, so the framework runs it when the calling instance tears down or the effect's inputs change. A unit that starts a subscription without registering its release leaks once per caller, and the leak scales with how widely the unit gets adopted.
  • How do two units compose, and does the inner one own anything?
    One simply calls the other. The inner unit's state and cleanup still belong to the same calling component instance, because the whole chain runs inside that instance's scope. That is also why a call-order rule, in runtimes that have one, applies transitively through the chain.

A recipe shared by two cooks does not produce one cake between them. Both follow the same steps, and each ends up with a separate cake in a separate tin.

saying these in an interview costs you the question

  • Thinks calling one unit in two components shares a single state
  • Expects a unit's state to outlive the component that called it
  • Hoists the state to a module-level variable and calls it an equivalent refactor
  • Calls the unit conditionally where state is keyed by call order
  • Believes logic must become a component or wrapper to hold state
  • Assumes a unit's timers and subscriptions clean themselves up unregistered