Repository navigation
Description
Describe the bug
A memo of isPending(count) reads differently while in transition
log points A , B
as expected, before transition and after transition
isPending(count) goes from false to true
log point C:
m1:false , m2:true
log point D:
after calling the action all of a sudden
m1:true , m2:false
log point E:
logging m1 and m2 again right after, both become false
m1:false, m2:false
Logs:
[log] A count false
[log] B count true
[log] C before action {m1: false, m2: true}
[log] action:start
[log] D after action A {m1: true, m2: false}
[log] E after action B {m1: false, m2: false}
[log] F {count: true}
[log] action:done
Your Example Website or App
Steps to Reproduce the Bug or Issue
repro
Expected behavior
I would expect a memo of isPending(count) to read more consistently
for example in log point C, we might say that m1 memo was created when isPending(count)
was still false, and it did not yet get a chance to "flush"
then at log point D why aren't they both true
and then at log point E
why are they both false, even though the transition is not done yet
as evident by log point F
Screenshots or Videos
Platform
.
Additional context
import { render, untrack } from '@solidjs/web';
import { createSignal, isPending, createMemo, action } from 'solid-js';
const act = action(function* () {
console.log("action:start")
yield new Promise(r => setTimeout(r, 3000))
console.log("action:done")
})
export default function App() {
const [count, setCount] = createSignal(0);
setTimeout(() => {
// created before transition
const m1 = createMemo(() => isPending(count));
console.log("A", "count", isPending(count))
setCount(v => v + 1) // start a transiton
// created after transition
const m2 = createMemo(() => isPending(count));
console.log("B", "count", isPending(count))
// logs m1,m2 - after transition
console.log(
"C","before action", {
m1: untrack(m1), // false
m2: untrack(m2) // true
})
// continue transiton for 3s using an action
act()
console.log(
"D",
"after action A", {
m1: untrack(m1), // true <-- flipped
m2: untrack(m2) // false <--
})
console.log(
"E", "after action B", {
m1: untrack(m1), // false <- some how it became false
m2: untrack(m2) // false
})
console.log("F",
{
count: isPending(count) // true
})
})
return (
<div class="p-2 whitespace-pre">
{`
a memo of an isPending, reads inconsistently
`}
</div>
);
}
if (typeof document !== 'undefined') {
render(() => <App />, document.getElementById('root')!);
}
Activity
Fix isPending memo inconsistency during action-held transitions (#3078)
ryansolid commentedon Aug 27, 2026
Fixed in 505c73d (will ship in the next rc). Great find — this was two stacked defects, and your repro shape (memos created without an owner, inside the
setTimeout) was the key ingredient.Why m1/m2 flipped at D/E: ownerless memos are observation-lifecycle (auto-dispose). An untracked read of one with no subscribers used to tear it down inline at the end of the read, and the next read revived it with a full recompute in whatever transition/lane context was ambient at that instant. So each of your
untrack(m1)calls was destructive — consecutive reads could answerfalse → true → falsewith no write in between. Reads now defer that teardown to a re-validating sweep at the next flush: reads are idempotent within a tick and the memory reclamation is unchanged (one microtask later).Why they read
falsewhileisPending(count)readtrue(your F): the fresh-read pairing rule ("a reader that saw the new value must not also be told it's pending") was firing for computes that saw the staged write while your action still held the transition. A staged signal write is an input to a computation still in flight — not a landed answer awaiting reveal — so the verdict is no longer suppressed there. Landed async answers keep the pairing rule.Fixed behavior for your exact code: C reads
{m1: false, m2: true}(m1 was created before the write and hasn't seen a flush yet — as you guessed), D/E are stable (no flips), and after the microtask flush everything agrees ontruefor the whole action window, thenfalseonce it settles.fix(signals): isPending() inside a memo reports a hold whose staged n…
fix(signals): a released memo's queued retry runs before the release (#…
fix(signals): a released memo's queued retry runs before the release (#…
fix(signals): a released memo's queued retry runs before the release (#…