Skip to content

[2.0 rc.16] A <Loading> mounted through latest() never reveals while an unread memo stays pending (regression from dc063a65, #4171) #4203

Closed

Description

@neokofg

Describe the bug

On 2.0.0-rc.16 a <Loading> stays on its fallback forever when all of these hold:

  • the <Loading> sits inside a <Show> whose condition reads latest(() => asyncMemo()) ?? asyncMemo(), so it is mounted by the verdict path;
  • a component inside it writes an ownedWrite signal during its mount;
  • it then creates a memo that reads that signal and returns a promise that never settles.

Nothing reads that memo, and the component renders static text. The docs say only expressions that read an async value wait for it. The signals rules say the same in RULES-FUNDAMENTALS.md, under Reader: "a pending value no render effect reaches holds nothing and reaches no boundary". So the boundary should reveal its content. On rc.13, rc.14 and rc.15 it does.

Your Example Website or App

Self-contained Node + jsdom script below (no compiler, no third-party libraries).

Steps to Reproduce the Bug or Issue

package.json:

{
  "private": true,
  "type": "module",
  "dependencies": { "solid-js": "2.0.0-rc.16", "@solidjs/web": "2.0.0-rc.16", "jsdom": "26.1.0" },
  "overrides": { "@solidjs/signals": "2.0.0-rc.16" }
}

repro.mjs:

import { JSDOM } from "jsdom";
const dom = new JSDOM("<!doctype html><div id=app></div>");
Object.assign(globalThis, { window: dom.window, document: dom.window.document, Node: dom.window.Node, HTMLElement: dom.window.HTMLElement });
const { createMemo, createSignal, createComponent, Loading, Show, latest } = await import("solid-js");
const { render } = await import("@solidjs/web");

function Child() {
  const [version, setVersion] = createSignal(0, { ownedWrite: true });
  setVersion(1);
  // never read by anything; stays pending forever
  createMemo(() => { version(); return new Promise(() => {}); });
  return "CHILD";
}

const el = document.getElementById("app");
render(() => {
  const gate = createMemo(async () => "x");
  return createComponent(Loading, { fallback: "OUTER LOADING", get children() {
    return createComponent(Show, { get when() { return latest(() => gate()) ?? gate(); }, children: () =>
      createComponent(Loading, { fallback: "INNER LOADING", get children() { return createComponent(Child, {}); } }) });
  } });
}, el);
setTimeout(() => { console.log(JSON.stringify(el.textContent)); process.exit(0); }, 300);
npm install && node --conditions=browser repro.mjs

Output (identical with --conditions=development):

solid-js / @solidjs/web / @solidjs/signals output
2.0.0-rc.13 "CHILD"
2.0.0-rc.14 "CHILD"
2.0.0-rc.15 "CHILD"
2.0.0-rc.16 "INNER LOADING" (forever)

With solid-js/web at rc.15 and only @solidjs/signals at rc.16, the output is "INNER LOADING", so the change is in signals.

Expected behavior

"CHILD". The inner boundary should reveal its content. The pending memo has no render-effect reader, so it should not hold the boundary. This is the rc.15 behavior.

Screenshots or Videos

No response

Platform

  • OS: macOS
  • Runtime: Node 25.8.2 + jsdom 26.1.0 (originally seen in Chromium in an app)
  • Version: solid-js / @solidjs/web / @solidjs/signals 2.0.0-rc.16; still reproduces on next@9f64ca93

Additional context

Works in rc.15, broken in rc.16.

Bisect. I built packages/signals (prod flags) at each signals commit between the rc.15 and rc.16 release merges, with solid-js and @solidjs/web held at rc.15:

  • first bad: dc063a6 fix(signals): a verdict-mounted Loading holds a nested render effect's first read (#4166) (#4171);
  • good: a2743fa2, the last signals change before it (its direct parent 7f8ab2db only touches web).

What is needed. Each item below turns the output back to "CHILD" on rc.16:

  • when={gate()} (no latest), or a synchronous gate;
  • writing the signal after creating the pending memo, or not writing it at all;
  • the pending memo not reading the written signal;
  • doing the write from a createEffect, createRenderEffect or timer instead of during the mount.

A more realistic shape that also reproduces. A data source whose memo starts its load and bumps an ownedWrite version signal on its first compute:

let started = false;
createMemo(() => { version(); if (!started) { started = true; setVersion(1); } return 1; });
createMemo(() => { version(); return new Promise(() => {}); }); // e.g. a disabled query's data: never settles, never read

This is how a query adapter can look: one query starts its fetch, and a sibling disabled query's data projection stays pending. In an app, every screen under such a latest-gated shell stays on the fallback.

Activity

  1. ryansolid commented on Oct 21, 2026

    @ryansolid
    Member

    Fixed by #4207, merged to next.

    — Claude via Claude Code


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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