Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

L2: mount triggered by latest() shows content beside the committed value

Closed
#3,851 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
55/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
typescript
Domain
frontend

Research direction

Start with the provided failing vitest repro under packages/signals/tests, run it to confirm slot reads "content 1" while screenX stays 0. Then read the first-pass lane rule in recompute (packages/signals/src/core/core.ts) alongside the SPEC's lane rule and A29 boundary-scope note to see why a pass created inside a verdict lane inherits that lane instead of staying mainline. Done means the repro passes (slot equals "fallback" during the hold) without breaking the existing lane, loading-boundary and optimistic-lane tests — expect to consult the SPEC draft for proto/3540-boundary-scope before touching semantics.

Written by the indexing model from the issue text.

Description

Summary

Inside an action, a <Show when={latest(x) > 0}> opens at once, which is expected, because latest() is display-ahead. But the new <Loading> it mounts shows content 1 while everything else on screen still shows the committed x = 0. Under the lane rule, a verdict lane's mounts stay mainline, so the mounted content belongs to the screen's world, not the proposal's. This is a torn frame.

Repro (packages/signals/tests, vitest)

import { expect, it } from "vitest";
import {
  action,
  createLoadingBoundary,
  createMemo,
  createRenderEffect,
  createRoot,
  createSignal,
  flush,
  latest,
  untrack
} from "../src/index.js";

const settle = async () => {
  for (let i = 0; i < 3; i++) {
    await new Promise(r => setTimeout(r, 0));
    flush();
  }
};

it("a mount triggered by latest() shows its content beside the committed value", async () => {
  const [x, setX] = createSignal(0);
  let screenX: unknown;
  let slot: unknown;
  createRoot(() => {
    // <p>{x()}</p>
    createRenderEffect(x, v => {
      screenX = v;
    });
    // <Show when={latest(x) > 0}>
    //   <Loading fallback="fallback">content {x()}</Loading>
    // </Show>
    const when = createMemo(() => latest(x) > 0);
    const children = createMemo(() =>
      when()
        ? untrack(() =>
            createLoadingBoundary(
              () => `content ${x()}`,
              () => "fallback"
            )
          )
        : "closed"
    );
    createRenderEffect(
      () => {
        const c = children();
        return typeof c === "function" ? c() : c;
      },
      v => {
        slot = v;
      }
    );
  });
  flush();
  expect([screenX, slot]).toEqual([0, "closed"]);

  let release!: () => void;
  const done = action(function* () {
    setX(1);
    yield new Promise<void>(r => (release = r));
  })();
  await settle();
  // The action holds x = 1; the screen still shows x = 0.
  expect(screenX).toBe(0);
  // Actual: "content 1" beside the committed 0.
  expect(slot).toBe("fallback");

  release();
  await done;
  await settle();
  expect([screenX, slot]).toEqual([1, "content 1"]);
});

The repro fails at expect(slot).toBe("fallback") with Received: "content 1". The same thing happens when the content is a memo, createMemo(() => \content ${x()}`)`.

Expected vs actual

Checkpoint Expected Actual
Before the action x = 0, slot closed x = 0, slot closed
Action holds x = 1 x = 0, slot fallback x = 0, slot content 1
Action ends x = 1, slot content 1 x = 1, slot content 1

Rules involved

  • The lane rule: an optimistic lane sees the screen plus its own guesses, and its mounts' children are the lane's. A verdict lane computes the real outcome, and its mounts stay mainline. Here the Show condition is verdict-lane work, and its children are seated in that lane. The new Loading's first pass inherits the lane from the pass that created it, so it reads the proposal x = 1.
  • A29's boundary scope (2026-10-06): a loading boundary that has not shown content owns its subtree. Content that reads a hold waits behind the boundary's fallback, and it appears at the hold's commit.
  • No tearing: content on screen agrees with the screen's value of what it reads.

The suspected site is the first-pass lane rule in recompute (core/core.ts). A first pass takes its creator's lane, which is right for an optimistic lane (ruling A). For a verdict lane, it should stay mainline.

Where it reproduces

  • next (ba67ddbaa)
  • #3835's branch (ae59c7682)
  • #3824's head (3ab71b0dc)
  • proto/3540-boundary-scope (60d2f1ded). Its SPEC draft already lists this shape as open ("a boundary a verdict reader mounts … shows the held derivation now rather than its fallback").

Provenance

Found by the semantic fuzzer's mount-under-hold cohort, verdict family (fuzz/semantic-fuzzer-l2, b2bdf125a, rules MH1 and MH5). It probably shares a root with the read-order mount family (fuzzer cases 827, 416 and 936). There, a latest() read routes a freshly mounted child into the verdict lane, its mount control publishes alone, and the slot stays empty. In both cases a pass picks up verdict-lane membership that the lane rule says stays mainline: through a read in the read-order family, and through its creator here.

Dominant language
TypeScript
Stars
36.1k
Forks
1.1k
Avg merge
11h 42m
Merged PRs (30d)
326

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from solidjs/solid

All issues in solidjs/solid

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.