Skip to content

[2.0 rc.14 – rc.19 / next] A memo born in a newly opened <Match> that reads latest() throws a stale <Match> read and halts reactivity #4341

Closed

Description

@neokofg

Describe the bug

Since 2.0.0-rc.14, a memo created inside a newly opened <Match> crashes the whole app if it reads latest() of an unrelated memo. The update that opens the <Match> also starts a sibling async memo under its own <Loading>. The engine then reads the <Match> callback's accessor after the branch has been closed, which throws "Attempting to access a stale value from that could possibly be undefined". Nothing catches the throw, so the reactive system halts (REACTIVITY_HALTED) and the screen stays on the previous branch for good.

Reading the same memo without latest() works.

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, Match, Switch } from "solid-js";
import { expect, it } from "vitest";

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

it("a memo created in a newly opened <Match> may read latest()", async () => {
  const el = document.createElement("div");
  const [ready, setReady] = createSignal(false);
  const zone = createMemo(() => (ready() ? "ready" : "loading"));
  const format = createMemo(() => "ru");
  function Picker(props) {
    // replacing latest(format) with format() makes the test pass
    const label = createMemo(() => `${latest(format)}:${zone()}:${props.value()}`);
    return <span>{label()}</span>;
  }
  render(() => {
    const data = createMemo(async () => {
      if (zone() !== "ready") return "none";
      await wait(30);
      return "data";
    });
    return (
      <>
        <Switch>
          <Match when={zone() === "loading"}><i>loading</i></Match>
          <Match when={zone() === "ready" && "sel"}>{(s) => <Picker value={s} />}</Match>
        </Switch>
        <Loading fallback={<b>wait</b>}><b>{data()}</b></Loading>
      </>
    );
  }, el);
  setTimeout(() => setReady(true), 10);
  await wait(150);
  expect(el.textContent).toBe("ru:ready:seldata");
});
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

ru:ready:seldata. The <Match> for ready opens, Picker shows ru:ready:sel, and the <Loading> shows data once it lands. This is what rc.13 renders.

Actual on rc.14 through rc.19 and on next@17d4d1b:

  • textContent is loadingnone;
  • the console shows Attempting to access a stale value from <Match> …, then [REACTIVITY_HALTED] An uncaught error halted the reactive system.

On rc.14 and rc.15, vitest also aborts the worker during teardown (Abort trap: 6).

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: a period picker inside a <Switch> halted the page.
  • Version: solid-js / @solidjs/web / @solidjs/signals 2.0.0-rc.19. Also reproduces on next@17d4d1b (signals built from source).

Additional context

solid-js / @solidjs/web / @solidjs/signals latest(format) format()
2.0.0-rc.13 passes passes
2.0.0-rc.14 … 2.0.0-rc.19 halts passes
next@17d4d1b halts passes

Bisect. I built packages/signals from source at each signals commit between the rc.13 and rc.14 release commits (35e0651ab7 … c421e199fa), with solid-js and @solidjs/web held at rc.14:

  • first bad: 1b9ceb6 signals: the hold model (L2) — core rebuilt from its rulings (#3774);
  • last good signals commit: 4f676973d3.

rc.19's #4281 (#4275) and #4304 (#4284) change how latest() behaves, but neither fixes this case.

Usage. Each piece is documented:

  • latest(fn): "Reads the freshest in-flight value available inside fn, falling back to the current settled value."
  • <Match> with a function child receiving the narrowed accessor.
  • createMemo returning a promise, read under <Loading>.

The component reads the accessor only from its own memo and does not keep it past unmount (no timers or async).

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