Why must the function that produces a component's output be pure, and what breaks when it is not?
answer
- the runtime decides how often it runs
- same inputs, same described output
- a pass's result can be discarded
- development double-run exposes impurity
- effects belong after the pass, not inside it
basics
~20 sBecause the runtime decides how often it runs: it may run it again for the same state, throw the result away, or skip it. Side effects inside it therefore happen an unpredictable number of times.
solid answer
~50 sThe runtime owns when output code runs, and it grants itself several liberties: running it again for the same inputs, running it at a different urgency, abandoning a half-finished pass, running it twice on purpose in development to expose impurity, and skipping it entirely when it can prove nothing relevant changed. Those liberties are only safe if the function is a mapping from its inputs - state, values passed in from the parent, values provided by an ancestor - to a description of output, with no observable effect of its own. What breaks it is familiar: writing state during the pass, mutating an input or a shared object, starting a request or subscription, reading or writing host nodes, and taking a value from the clock or a random source. Each of those belongs in the effect phase or in a stable initialiser instead.
go deeper
Recall what belongs in the code that describes output: reading values and returning a description. Requests, timers and node manipulation go somewhere else.
Explain the liberties purity buys the runtime - re-running, discarding, skipping, double-running in development - and name the anti-pattern that breaks each one.
Catch impurity in review and in incidents: duplicated requests, counters that drift, output that differs between two passes with identical state, and mutations that never trigger an update.
Decide how the codebase enforces it: where derived data is computed, where effects live, and whether development double-invocation stays on, since it is the cheapest detector you have.
"Pure" here is a narrow, practical contract, not a philosophical stance: **given the same inputs, the function describes the same output and does nothing else observable.** The reason it is a hard requirement rather than a style preference is that the runtime, not your code, decides how often that function runs. ## The liberties the runtime takes 1. **Run it again for inputs that did not change.** A parent update, a changed provided value, or a store notification can trigger a pass in which an instance's own inputs are identical. A runtime is free to re-run rather than prove equality first. 2. **Throw the result away.** A pass can be abandoned - because a more urgent update arrived, because something further up changed again, or because the work was speculative. Output described by that pass never becomes visible. 3. **Run it at a different urgency, later.** The same instance can be rendered twice for one user action: once for an urgent update, once for deferred work. 4. **Skip it.** When the runtime can prove the inputs are equal, or a subtree is marked as not needing work, the function may not run at all for a given update. 5. **Run it twice deliberately in development.** Several frameworks offer a development mode that invokes output code twice and discards one result, precisely so impurity shows up on a developer's machine instead of in production. Every one of these is safe for a function that only reads and returns. Every one of them corrupts a function that also *does* something. ## What breaks, and where it belongs instead | impurity in output code | symptom it produces | where the work belongs | |---|---|---| | starting a request or subscription | duplicate requests, leaked subscriptions, work on discarded passes | the effect phase, keyed to the inputs it depends on | | writing state during the pass | extra passes, loops that only sometimes terminate | derive the value instead, or write it from an event or effect | | mutating an input or shared object | changes that never trigger an update, values that differ per pass | produce a new value; treat inputs as read-only | | reading or writing host nodes | reads of the previous commit, or of a tree that never becomes visible | after the update has been applied | | reading the clock or a random source | two passes describe different output from identical state | capture the value once at the event, or in a stable initialiser | | incrementing a module-level counter | numbers that drift by how often the runtime happened to render | keep the counter in state or outside the render path | ## Idempotence, and why it is the stricter half Purity says the function has no observable effect. Idempotence says running it twice is indistinguishable from running it once. The runtime needs both, because the second run is the one it may perform and discard. A function that appends to an array it received, for instance, is not obviously "effectful" in the everyday sense - nothing is fetched, nothing is logged - yet two runs give a different array than one, and which of the two the user sees depends on scheduling. That is why "it worked when I tested it" is no evidence at all: it worked under the schedule you happened to get. ## The cases where you genuinely need something impure - **A timestamp or an identifier.** Read it once where the event happens and store it, so every pass renders the stored value. Generating it during the pass makes output non-deterministic and, in a keyed list, discards instances on every render. - **A derived value that is expensive.** Compute it from inputs with memoisation on the inputs, not by caching into a variable outside the function that later passes read. - **Logging while debugging.** Acceptable, but expect duplicates; a doubled log under a development double-run is the tool working, not a framework bug. ## Does a fine-grained runtime escape this? It narrows the scope but keeps the rule. A runtime that tracks dependencies per computation does not re-run a whole component function, so fewer things are re-executed - but derived computations still re-run whenever a dependency changes, may be evaluated lazily or not at all if nobody reads them, and may be recomputed more often than a reading of the code suggests. A derivation with a side effect inside it therefore fires an unpredictable number of times, for the same reason and with the same class of bug. Compile-time runtimes are no different: the compiler moves the bookkeeping, not the requirement. ## How to police it - In review, read output code for verbs: fetch, subscribe, set, push, sort in place, write, focus, scroll, measure. A verb that acts on the world is the smell. - Leave the development double-invocation mode on if your framework has one; it is the cheapest impurity detector available and it finds bugs no test asserts. - Treat sorting or reversing a received array in place as a defect even when it appears to work - the array is someone else's value, and the pass may be discarded after the damage is done.
- Why do some frameworks deliberately run output code twice in development?Because impurity is invisible while the function happens to run once. Running it twice and discarding one result makes a duplicated request, a doubled counter or a mutated shared array visible on the developer's machine, rather than waiting for a production schedule that re-renders and exposes it under load.
- Is reading the current time inside output code always wrong?It is wrong whenever the rendered value is expected to be stable, because two passes with identical state then describe different output, and which one the user sees is decided by scheduling. Read the clock when the event happens, store the value, and render from what is stored.
- Does a fine-grained runtime that never re-runs a whole component still need purity?Yes, at a smaller grain. Its derived computations re-run when a dependency changes, may be evaluated lazily or skipped when nothing reads them, and can recompute more often than expected, so a side effect inside a derivation fires an unpredictable number of times. The scope shrinks; the rule does not.
- Mutating a received array sometimes appears to work. Why is it still a defect?Because the value belongs to whoever passed it, and the mutation survives even when the pass that made it is discarded. Later comparisons against that array can no longer detect a change, so an update silently fails to appear. Produce a new array and leave the input untouched.
saying these in an interview costs you the question
- Says purity is a style preference with no runtime consequence
- Starts a request in output code and relies on it running once
- Mutates a value passed in and expects an update to follow
- Treats a doubled development log as a framework bug
- Writes state during the pass to compute a derived value
- Thinks purity only matters where passes can be abandoned