Describe the bug
Hi I am trying to learn SolidJS for the first time with 2.0, and I ran in to what seems to either be unintuitive / inconsistent behavior of latest() or a bug.
Your Example Website or App
https://stackblitz.com/edit/solidjs-templates-93va2vbn?file=src%2FApp.tsx
Steps to Reproduce the Bug or Issue
import { createEffect, createSignal, createMemo, Loading, isPending, latest } from 'solid-js';
// Self-resolving fake fetch so the demo needs no network.
const fakeFetch = (name: string) =>
new Promise<{ name: string }>((r) => setTimeout(() => r({ name }), 1000));
export default function App() {
const [query, setQuery] = createSignal('pikachu');
const pokemon = createMemo(() => fakeFetch(query()));
const pending = () => isPending(() => pokemon());
// Un-commenting this works around the issue
/*
createEffect(
() => latest(() => query()),
() => {}
); // warm the companion at mount
*/
return (
<div>
<button onClick={() => setQuery('pikachu')}>pikachu</button>
<button onClick={() => setQuery('charizard')}>charizard</button>
<button onClick={() => setQuery('gengar')}>gengar</button>
<Loading fallback={<div>skeleton</div>} on={query}>
<div>Current: {pokemon().name}</div>
{/* latest(query) is NOT evaluated during initial load: the
short-circuit means it is first called DURING the first transition. */}
{pending() && (
<p>
Now Loading: <b>{latest(() => query())}</b>
</p>
)}
</Loading>
</div>
);
}
Clicking between the buttons seems to, somewhat non-deterministically, display the previous pokemon's name instead of the upcoming pokemon's name.
Expected behavior
I would expect latest(() => query()) to always show the latest value -- and it does, if the shown work-around is commented out.
Screenshots or Videos
No response
Platform
Linux, Firefox
Additional context
LLM indicated the issue as being this:
In @solidjs/signals (dist/dev.js of @solidjs/signals@2.0.0-rc.1):
-
latest() → latestRead(el) reads a per-source latest-value companion
(el._latestValueComputed), created lazily by getLatestValueComputed on first use:
// dev.js:4096
function getLatestValueComputed(el) {
if (!el._latestValueComputed) {
...
el._latestValueComputed = optimisticComputed(() => read(el));
...
}
return el._latestValueComputed;
}
Note: no backfill. The companion's compute read(el) is first executed (or forced
by latestRead's prepareComputed) after the pending write was already processed.
Inside the held transition's render context (stale = true for render effects, set at
dev.js:2587-2589), read(el) returns the committed value.
-
When a write is processed during a transition, the companion is updated only if it
already exists (syncCompanions, dev.js:4030):
function syncCompanions(el, value) {
if (el._pendingSignal) updatePendingSignal(el);
if (el._latestValueComputed) setSignal(el._latestValueComputed, value);
}
The app's banner is gated, so the companion does not exist at write time on the
first transition; it is created afterwards with the committed value and never learns
the in-flight write.
-
Compare getPendingSignal (dev.js:3909), which does backfill:
function getPendingSignal(el) {
if (!el._pendingSignal) {
el._pendingSignal = optimisticSignal(false, { ownedWrite: true });
el._pendingSignal._parentSource = el;
if (computePendingState(el)) setSignal(el._pendingSignal, true); // backfill
}
return el._pendingSignal;
}
This is why isPending() is correct on first use during a transition while
latest() is not.
Suggested framework fix
In getLatestValueComputed, backfill the current in-flight value when the companion is
created, mirroring getPendingSignal — e.g., if the source currently carries a pending
write (el._pendingValue !== NOT_PENDING or the equivalent optimistic-lane read),
initialize the companion to that value rather than only to read(el).
Describe the bug
Hi I am trying to learn SolidJS for the first time with 2.0, and I ran in to what seems to either be unintuitive / inconsistent behavior of latest() or a bug.
Your Example Website or App
https://stackblitz.com/edit/solidjs-templates-93va2vbn?file=src%2FApp.tsx
Steps to Reproduce the Bug or Issue
Clicking between the buttons seems to, somewhat non-deterministically, display the previous pokemon's name instead of the upcoming pokemon's name.
Expected behavior
I would expect
latest(() => query())to always show the latest value -- and it does, if the shown work-around is commented out.Screenshots or Videos
No response
Platform
Linux, Firefox
Additional context
LLM indicated the issue as being this:
In
@solidjs/signals(dist/dev.jsof@solidjs/signals@2.0.0-rc.1):latest()→latestRead(el)reads a per-source latest-value companion(
el._latestValueComputed), created lazily bygetLatestValueComputedon first use:Note: no backfill. The companion's compute
read(el)is first executed (or forcedby
latestRead'sprepareComputed) after the pending write was already processed.Inside the held transition's render context (
stale = truefor render effects, set atdev.js:2587-2589),
read(el)returns the committed value.When a write is processed during a transition, the companion is updated only if it
already exists (
syncCompanions, dev.js:4030):The app's banner is gated, so the companion does not exist at write time on the
first transition; it is created afterwards with the committed value and never learns
the in-flight write.
Compare
getPendingSignal(dev.js:3909), which does backfill:This is why
isPending()is correct on first use during a transition whilelatest()is not.Suggested framework fix
In
getLatestValueComputed, backfill the current in-flight value when the companion iscreated, mirroring
getPendingSignal— e.g., if the source currently carries a pendingwrite (
el._pendingValue !== NOT_PENDINGor the equivalent optimistic-lane read),initialize the companion to that value rather than only to
read(el).