Skip to content

latest() returns the committed value instead of the in-flight value on first use during a held transition #3041

Description

@tehsausage

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions