skip to content

Why can a child component not treat a change in the handler it received as a signal that its caller changed something?

level: seniorimportance: should knowfreq 46%

answer

  1. identity belongs to the runtime, not the caller
  2. re-run body means a fresh function
  3. run-once body means one reference forever
  4. pass intent as an explicit input

basics

~20 s

Handler identity reflects the runtime's execution model, not the caller's intent. Where the component body re-runs, a fresh function crosses the boundary every update; where the body runs once, one reference is passed for the instance's life.

solid answer

~50 s

Whether the function a child receives is the same object as last time is decided by how the framework runs component code. In a runtime that re-invokes the component body on every update, an inline handler is a new function each time, so inequality is the normal case and proves nothing about intent. In a runtime whose body runs once, the same reference is handed down for the life of the instance, so equality is the normal case and proves nothing either, since the values it closes over keep changing underneath it. A child branching on "the handler changed" is reading a property of the framework, and the same component behaves differently on another one. Pass intent explicitly: a mode input, a generation counter, or a key that forces a fresh instance. And stabilising a reference does not refresh what it captured.

go deeper

for a junior

Know that the function a child receives may be a brand-new object on every update or the very same one forever, depending on the framework, so it says nothing about what the caller meant.

for a middle

Explain both models: a re-invoked body creates a fresh closure each update, a body that runs once hands down one reference for the instance's life, and neither encodes intent.

for a senior

Show what you pass instead - a mode input, a generation value, a key that replaces the instance - and diagnose the mirror case where a stabilised reference carries a stale capture.

for a principal

Treat identity as an implementation detail no contract should depend on. Guidance that generalises one runtime's behaviour becomes wrong the moment a team adopts another.

"Is passing an inline handler down expensive?" has no answer until you say which runtime, and the same is true of the subtler question a child sometimes asks: *did my caller give me a different handler this time?* Identity across the component boundary is a property of the execution model, not of the caller's meaning. ## Where identity comes from A handler is a function value. Two things determine the value that crosses the boundary on a given update: 1. **Whether the code that creates it ran again.** A function literal evaluated a second time produces a second, distinct function object, even when the source text and the captured values are identical. 2. **Whether the author deliberately preserved a reference** - stored it somewhere stable, or let the framework do so. ## Two execution models, two normal cases | | Body re-invoked each update | Body runs once per instance | |---|---|---| | An inline handler literal | recreated on every update | created once, reused forever | | Identity across updates | normally different | normally identical | | What the closure captured | whatever this run saw | whatever the single run saw | | "Did the handler change?" | almost always yes, meaninglessly | almost always no, meaninglessly | In the first model, a child comparing handlers sees a change on every update regardless of intent. In the second, it sees no change even when everything the handler will act on has changed. **Both answers are wrong for the same reason: identity is not carrying the information the child wants.** Compile-time and dirty-checking runtimes land on one side or the other of this table depending on how they emit or invoke component code, which is precisely why the property is not portable. ## Why the comparison fails in both directions - **Inequality does not mean intent.** A caller that changed nothing still hands down a new function in a re-running model. A child that resets state, refetches or re-registers on that basis will do so continuously. - **Equality does not mean stability.** In a run-once model the single function object closes over whatever it captured, and the meaning of calling it can change completely while the reference stays put. - **Neither is a contract.** Nothing in a component's declared interface says "the handler input changes when X changes". Callers are free to inline, hoist, memoise or rebuild it. What such a change *costs* in rendering work is a separate question, and not the one being asked here: the point is only what identity can tell you, which is nothing about the caller's intent. ## What to pass instead 1. **An explicit input for the intent.** If the child must react to "the caller switched data sources", pass the source identifier. Identity is a side channel; a named input is a contract. 2. **A generation or version value.** When the caller genuinely wants to say "start over", an incrementing value it owns says it loudly and survives any runtime. 3. **A key that forces a new instance.** When starting over means discarding all local state, replacing the instance is clearer than persuading the old one to reset. 4. **Nothing at all.** Most children have no business knowing; they call the handler they were given and let the caller decide. ## The stale-capture mirror image The reverse mistake is just as common. A handler is deliberately stabilised so its reference does not change - and stabilising a reference does **not** refresh the values it captured. The stable function keeps the view of the world it had when it was created, so it can act on a value from an earlier update while looking perfectly well-behaved. The fixes are the usual ones: have the handler read current state at call time rather than closing over a snapshot, or express the update relative to the previous value instead of the captured one. ## Interview signal The strong answer names the execution model as the cause, gives both directions of failure - a fresh closure every update versus one reference forever - and then says what to pass instead. It also flags the mirror image, because a candidate who knows that a stabilised reference can carry a stale capture has actually debugged this rather than read about it. The weak answer generalises whichever runtime they used first, which is how a component that behaves correctly in one codebase becomes a mystery in the next.

  • If a handler reference is deliberately kept stable, what problem can that introduce?
    A stale capture. Stabilising the reference does not refresh the values the function closed over, so it can act on data from an earlier update while looking healthy. Read current state at call time, or express the change relative to the previous value, rather than trusting what was captured.
  • How should a caller tell a child to start over?
    With something explicit: an incrementing generation value the child watches, or a key that replaces the instance outright when all local state should go. Both read as intent in the component's contract and behave the same on every runtime, unlike a change in handler identity.
  • Does a framework's own comparison of inputs make handler identity meaningful?
    No. It tells the framework whether the value it holds differs, which is exactly the runtime-dependent fact - new closure every update, or one reference forever. That is information about the execution model, not about what the caller meant, so application logic still should not branch on it.

saying these in an interview costs you the question

  • Assumes an inline handler literal keeps the same identity across updates
  • Assumes a handler reference changes whenever the caller's data changes
  • Builds child behaviour on comparing the previous handler with the new one
  • Thinks stabilising a reference also refreshes the values it captured
  • Generalises one runtime's identity behaviour to every framework