Repository navigation
Closed
Description
Describe the bug
In the reproduction, we can see that after a cleanup of the component
the signal value logs the value initially passed to prop.val
if we enable the "eval" log we will also see it evaluation on the flush of the cleanup.
the question is why will a cleanup trigger reevaluation of the signal source function
if the prop value never changed, in that process.
Your Example Website or App
Steps to Reproduce the Bug or Issue
see reproduction code instructions included
Expected behavior
signal created in the child component should retain the last value it was set with
on component cleanup
there might be code that will run after cleanup that might still need the last value set
and this creates an unexpected behavior for not apparent reason.
Screenshots or Videos
No response
Platform
.
Additional context
import { render } from '@solidjs/web';
import { createSignal, onSettled, flush, Show, refresh } from 'solid-js';
function Value(props) {
// signal with function
const [sA, setA] = createSignal(() => {
//console.log("eval")
return props.val
})
// signal with value
const [sB, setB] = createSignal(111)
return <>
<div>
<button
class="bg-purple-500 hover:bg-purple-700 text-white font-bold py-1 px-2 rounded" onClick={() => {
setA(333)
flush()
console.log(sA()) // 333
props.inc() // inc to toggle off
flush() // flush
console.log(sA()) // 0
}}>
A {sA()}
</button>
</div>
<div>
<button class="bg-orange-500 hover:bg-orange-700 text-white font-bold py-1 px-2 rounded" onClick={() => {
setB(666)
flush()
console.log(sB()) // 666
props.inc() // inc to toggle off
flush() // flush
console.log(sB()) // 666
}}>
B {sA()}
</button>
</div>
</>
}
const preMessage = `1. click A
logs:
333
0 <- reverted to props.val due to reevaluation
2. Click "Toggle On"
2.1 click B
logs:
666
666
`
export default function App() {
const [count, setCount] = createSignal(0);
return (
<div class="p-2">
<pre class="text-sm">
{preMessage}
</pre>
<button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" onClick={() => setCount(count() + 1)}>
{count() % 2 == 0 ? "toggle off" : "toggle on"}
</button>
<Show when={count() % 2 === 0}>
{() => {
return <Value inc={() => setCount(v => v + 1)} val={0}></Value>
}}
</Show>
</div>
);
}
if (typeof document !== 'undefined') {
render(() => <App />, document.getElementById('root')!);
}Metadata
Metadata
Assignees
Labels
No labels
Activity
[-]2.0.0-rc | signal source function revaluates when derived from a component prop - on component cleanup[/-][+]2.0.0-rc.1 | signal source function revaluates when derived from a component prop - on component cleanup[/+]fix(signals): owner disposal is death — reads freeze instead of reawa…
ryansolid commentedon Aug 20, 2026
Fixed on
nextin 3995787 — this will land in the next rc.What was happening. Reading a disposed computed re-ran its source function (the same path lazy memos use for first reads). When the
Showbranch toggled off, the component's owner tree was disposed, and your next read ofsA()re-derived() => props.val— discarding the committed 333 and returning the seed. The "eval" you logged was that disposed read re-deriving, not the cleanup itself. Your value-form signal (sB) was unaffected because plain signals aren't owned — there's nothing to dispose.The new semantics. Owner disposal is now death for everything it reaches: reads after teardown return the last committed value and never re-run the source function. Your expectation is exactly the implemented behavior — code running after cleanup (event handlers, async continuations) sees the last value the signal held, written or computed. This also stops post-teardown reads from re-executing user code against a disposed tree (e.g. re-firing async work with no owner).
The one lifecycle that still recomputes on read is deliberate and unrelated to components: an unowned memo that was released because its last subscriber left goes dormant, and reading it reawakens it with a fresh value — that's the pay-for-use contract for unowned/lazy computeds. Anything created under an owner is frozen once the owner is gone.
Thanks for the clean reproduction — it isolated this precisely.
refactor(signals): dispose() delegates to unobserved() — the teardown…
fix(signals): freeze a dormant lazy memo read under a dead owner
fix(signals): freeze a dormant lazy memo read under a dead owner
fix(signals): a reawakened lazy memo rejoins its owner's chain (#3555)
fix(signals): a parked frame dies with its owner — root disposal mid-…
fix(signals): a parked frame dies with its owner — root disposal mid-…
fix(signals): recompute preserves REACTIVE_DISPOSED set during the run (