RMD022 — A hook's props callback built a new value for the same contents
The props callback is called twice in the same tick and the two bags compared — the
same check RMD020 runs on
render(), on the other place the framework asks the app for a value. It is part of the
strict render, so configureDev({ strictRender: false }) turns both off.
Why it matters more here than it looks: every prop is a signal, and a signal compares
by reference. A rebuilt array is a changed prop, so a @compute reading it recomputes, a
@watchProp on it fires, and a subscription whose connect reads it reconnects — every
time the callback runs.
Two conditions, not one. The same-tick pair proves a value was built in place. That
alone is not worth saying: key: ["user", self.props.id] is built in place too, and when
id moves the array genuinely differs from last time — so the fix below would hand back
nothing. The second condition is a count across runs: this prop was rebuilt on four
consecutive runs of the callback and its value never moved. Below four, ordinary code gets
reported for coincidences; the same threshold, for the same reason, as
RMD024.
A corollary worth knowing: a callback that is never invalidated cannot be reported for churn. It runs once, its bag is cached, and a value built once is not churn — the count never leaves zero. The different-contents finding below still applies to it, and matters more there than anywhere: a value that is not a function of state is frozen into that cache at mount and served for the life of the hook.
Three findings, three fixes:
- an array or object — hold it somewhere that has an identity (a
@compute, a field, a module constant) and hand that over, so the callback passes a value along instead of building one. If you own the hook,@StablePropsdeclares the prop a value and settles it for every call site at once. - a function — a bound method (
fetch: self.load) readsthiswhen it is called, so there is nothing to capture and the identity never changes;@memoizedwhen it has to be built per argument. A declaration cannot help here: two closures with the same body are not equal by any comparison that is safe to make, so a declared function prop is still reported. - different contents from two calls in one tick — the callback is not a function of
state. Read the value once in
@createdand keep it in@state, or read it where it is needed. Nothing can hide this one; what is compared is the contents. Reported on the first occurrence, with no count in front of it: this is a fault rather than churn, and it is the one kind the cache makes worse — aMath.random()in the bag is now frozen into the cached bag until something else invalidates it.
A @compute holding the whole bag fixes every value in it at once, and is the shortest
answer when several are unstable together.
Next
- All diagnostics — every code the framework can report.
- Checking your app — the faults proved from the source, before anything runs.