2.0.0-rc.20: <Loading> stays on its fallback forever when an async memo's first answer is superseded while a sibling under the same boundary refetches

Chiusa
#4,541 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
25/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
typescript
Ambito
frontend

Direzione di ricerca

Start by running the reproduction as a Vitest test with jsdom, and confirm that the control case passes while the sibling-refetch case shows <p>wait</p>. The bug sits in how a Loading boundary tracks pending async memos when a first promise is superseded by a synchronous recomputation, so the fix lives in the reactive core, not in the components shown. Done means both cases in the repro render <i>B</i><b>A</b> and the boundary recovers after later writes.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

Describe the bug

Two async memos a and b are read under one <Loading>. b is on its first load. While it loads, a starts a refetch (returns a new promise). b's first answer then lands in the order data libraries use: the value is stored and the memo's source is bumped before the first promise's continuation reaches the core, so the first flight is superseded by a synchronous recomputation. a's refetch then settles.

Both memos now hold settled values, but <Loading> shows its fallback forever. Without the sibling refetch the same sequence renders the content.

  • rc.13: renders "" (also wrong)
  • rc.20: <p>wait</p> forever
  • next@215c365: <p>wait</p> forever
Steps to Reproduce

Same setup as above. repro.test.tsx:

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

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

async function run(refetchA: boolean): Promise<string> {
  const [aVersion, setAVersion] = createSignal(0);
  const [bVersion, setBVersion] = createSignal(0);
  let aFlight: Promise<string> | null = null;
  let releaseA: (v: string) => void = () => {};
  let bValue: string | null = null;
  let releaseB: (v: string) => void = () => {};
  const bFlight = new Promise<string>((resolve) => (releaseB = resolve));

  const el = document.createElement("div");
  const dispose = render(() => {
    const a = createMemo(() => (aVersion(), aFlight ?? "A"));
    const b = createMemo(() => (bVersion(), bValue ?? bFlight.then((v) => v)));
    return (
      <Loading fallback={<p>wait</p>}>
        <Show when={b()}>
          <i>{b()}</i>
        </Show>
        <Show when={a()}>
          <b>{a()}</b>
        </Show>
      </Loading>
    );
  }, el);
  await wait(10);
  expect(el.innerHTML).toBe("<p>wait</p>");

  if (refetchA) {
    aFlight = new Promise<string>((resolve) => (releaseA = resolve));
    setAVersion(1);
  }
  await wait(0);

  // b's first answer: the value is written and its version bumped before the
  // first promise's continuation reaches the core (the order TanStack Query uses).
  bValue = "B";
  releaseB("B");
  setBVersion(1);
  await wait(20);

  if (refetchA) {
    aFlight = null;
    releaseA("A");
    setAVersion(2);
  }
  await wait(50);
  const html = el.innerHTML;
  dispose();
  return html;
}

it("control: without a sibling refetch the content renders", async () => {
  expect(await run(false)).toBe("<i>B</i><b>A</b>");
});

it("with a sibling refetch in the same window the content renders once both settle", async () => {
  expect(await run(true)).toBe("<i>B</i><b>A</b>");
});

package.json

{
  "name": "solid-repro",
  "private": true,
  "type": "module",
  "scripts": { "test": "vitest run" },
  "dependencies": {
    "solid-js": "2.0.0-rc.20",
    "@solidjs/web": "2.0.0-rc.20",
    "@solidjs/signals": "2.0.0-rc.20"
  },
  "devDependencies": {
    "@solidjs/vite-plugin": "3.0.0-next.54",
    "jsdom": "30.1.2",
    "vitest": "5.0.5"
  }
}

vitest.config.ts

import solid from "@solidjs/vite-plugin";
import { defineConfig } from "vitest/config";

export default defineConfig({
  plugins: [solid()],
  test: { environment: "jsdom" },
});
Expected behavior

<i>B</i><b>A</b> in both cases.

Actual behavior

Control passes; with the sibling refetch the boundary stays at <p>wait</p> and never recovers (no further writes revive it).

Platform

As above.

Lingua principale
TypeScript
Stelle
36.2k
Fork
1.1k
Merge medio
11h 5m
PR unite (30g)
475

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di solidjs/solid

Tutte le issue di solidjs/solid

Issue simili

Altre issue su TypeScript

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.