Skip to content

[2.0 rc.16 – rc.19 / next] Incomplete #4207 fix: an unread pending memo behind an intermediate memo still holds a latest()-mounted <Loading> #4340

Closed

Description

@neokofg

Describe the bug

This is the case #4203 describes, with one extra memo between the written signal and the never-settling memo. The fix for #4203 (#4207, 0117658bee) does not cover it.

A <Loading> mounted by a keyed <Show> whose condition reads latest(value) ?? value() stays on its fallback forever when the component inside it:

  1. writes an ownedWrite signal during mount;
  2. derives a memo from that signal;
  3. creates a memo that reads the derived memo and returns a promise that never settles.

Nothing reads the pending memo, and the component renders static text. As in #4203, a pending value that no render effect reaches should not hold the boundary.

Your Example Website or App

Inline below: a self-contained vitest + jsdom test, solid-js / @solidjs/web only.

Steps to Reproduce the Bug or Issue

repro.test.jsx:

import { render } from "@solidjs/web";
import { createMemo, createSignal, latest, Loading, Show } from "solid-js";
import { expect, it } from "vitest";

const wait = (ms) => new Promise((r) => setTimeout(r, ms));

it("an unread pending memo behind an intermediate memo does not hold a latest()-mounted <Loading>", async () => {
  const el = document.createElement("div");
  function Child() {
    const [version, setVersion] = createSignal(0, { ownedWrite: true });
    setVersion(1);
    const gate = createMemo(() => version()); // reading version() directly here passes since #4207
    createMemo(() => (gate(), new Promise(() => {}))); // never read, never settles
    return <p>child</p>;
  }
  render(() => {
    const value = createMemo(async () => "v");
    return (
      <Loading fallback={<p>outer</p>}>
        <Show when={latest(value) ?? value()} keyed>
          {() => <Loading fallback={<p>inner</p>}><Child /></Loading>}
        </Show>
      </Loading>
    );
  }, el);
  await wait(100);
  expect(el.textContent).toBe("child");
});
Setup (package.json + vitest.config.mjs)
{
  "private": true,
  "type": "module",
  "dependencies": {
    "solid-js": "2.0.0-rc.19", "@solidjs/web": "2.0.0-rc.19", "@solidjs/signals": "2.0.0-rc.19",
    "@solidjs/babel-plugin": "2.0.0-rc.19", "@babel/core": "7.28.5",
    "vitest": "3.2.4", "vite": "7.1.12", "jsdom": "26.1.0"
  },
  "pnpm": { "overrides": { "solid-js": "2.0.0-rc.19", "@solidjs/web": "2.0.0-rc.19", "@solidjs/signals": "2.0.0-rc.19" } }
}
// vitest.config.mjs
import { transformAsync } from "@babel/core";
import solid from "@solidjs/babel-plugin";
const conditions = ["browser", "development"];
export default {
  plugins: [{
    name: "solid-jsx", enforce: "pre",
    async transform(code, id) {
      if (!id.endsWith(".jsx")) return null;
      const r = await transformAsync(code, { filename: id, babelrc: false, configFile: false, sourceMaps: true,
        plugins: [[solid, { generate: "dom", hydratable: false }]] });
      return { code: r.code, map: r.map };
    },
  }],
  resolve: { conditions },
  ssr: { resolve: { conditions, externalConditions: conditions }, noExternal: [/solid-js/, /@solidjs\//] },
  test: { environment: "jsdom", server: { deps: { inline: [/solid-js/, /@solidjs\//] } } },
};

Run: pnpm install && pnpm vitest run repro.test.jsx. Swap all four Solid versions (and the overrides) to test another RC.

Expected behavior

child, as on rc.13 – rc.15. Actual on rc.16 – rc.19 and on next@17d4d1b: inner, forever.

Each of these makes the test pass on rc.19:

Screenshots or Videos

No response

Platform

  • OS: macOS
  • Runtime: Node 25.8.2, vitest 3.2.4, jsdom 26.1.0, @solidjs/babel-plugin (generate dom). Originally seen in Chromium: screens under a latest-gated shell never leave the fallback when a sibling query is disabled.
  • Version: solid-js / @solidjs/web / @solidjs/signals 2.0.0-rc.19. Also reproduces on next@17d4d1b (signals built from source).

Additional context

@solidjs/signals (built from source) #4203 shape (no gate memo) this shape (with gate memo)
5c0d1905d5 (parent of #4207) inner inner
0117658bee (#4207) child inner
2.0.0-rc.19 (npm), next@17d4d1b child inner

Bisect. Signals built from source between the rc.15 and rc.16 release commits, with solid-js and @solidjs/web at rc.16. The first bad commit is dc063a6 fix(signals): a verdict-mounted Loading holds a nested render effect's first read (#4166) (#4171), the same commit as #4203.

Usage. The signal options reference describes ownedWrite as "Suppress dev-mode warnings when writing inside an owned scope". The migration guide says: "Reserve ownedWrite: true for an internal signal that intentionally accepts in-scope writes." A data-fetching adapter is that case. It bumps a version signal while its first compute runs, and a sibling disabled query's data memo derives from it and stays pending.

Activity

  1. ryansolid commented on Oct 28, 2026

    @ryansolid
    Member

    Fixed on next by #4350 (409f893); it ships in the next RC.


    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