Skip to content

[2.0 rc.14 – rc.19 / next] Render effects under a nested <Loading> are deferred until its data resolves when the outer <Show> opens on an async memo, but not on a signal #4343

Closed

Description

@neokofg

Describe the bug

Since 2.0.0-rc.14, when a render effect runs depends on how its subtree was opened. A component under a nested <Loading>, inside a <Show>, has a createRenderEffect and reads an async memo. The inner <Loading> shows its fallback in both cases below.

  • If the <Show> opens because a signal changes, the render effect runs at once while the inner <Loading> shows its fallback. This matches rc.13.
  • If the <Show> opens because an async memo settles, the render effect does not run until the inner <Loading>'s data resolves. The inner fallback is on screen the whole time.

On rc.13 the render effect runs at once in both cases. createEffect waits for the data in both cases (checked on rc.13 and rc.19), so only render effects changed, and only under the async gate.

This matters for libraries that subscribe from a render effect. @tanstack/solid-query attaches its observer that way. Under the async gate the observer attaches only after the first fetch lands, finds the result stale (with staleTime: 0), and fetches again: every screen behind an async auth gate makes two requests instead of one.

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, createRenderEffect, createSignal, Loading, Show } from "solid-js";
import { expect, it } from "vitest";

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

for (const gate of ["async", "signal"])
  it(`render effect under an inner <Loading> runs before its data resolves (gate: ${gate})`, async () => {
    const el = document.createElement("div");
    const log = [];
    function Screen() {
      const data = createMemo(async () => { await wait(50); log.push("data"); return "v"; });
      createRenderEffect(() => 0, () => { log.push("effect"); });
      return <p>{data()}</p>;
    }
    const [open, setOpen] = createSignal(false);
    render(() => {
      const ready = gate === "async" ? createMemo(async () => { await wait(10); return true; }) : open;
      return (
        <Loading fallback="outer">
          <Show when={ready()}>
            <Loading fallback="inner"><Screen /></Loading>
          </Show>
        </Loading>
      );
    }, el);
    if (gate === "signal") setTimeout(() => setOpen(true), 10);
    await wait(30);
    expect(el.textContent).toBe("inner");
    expect(log).toEqual(["effect"]);
    await wait(100);
    expect(log).toEqual(["effect", "data"]);
  });
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

Both gates behave the same way. With the inner fallback on screen at 30 ms, the render effect has already run (["effect"]), as it does on rc.13, and as it still does on rc.19 under the signal gate.

Actual on rc.14 – rc.19 and on next@17d4d1b (gate: async): log is [] at 30 ms, and the render effect runs only after data resolves.

If deferring render effects behind a fallback is the intended rule now, the signal gate breaks it. In that case, please say which of the two is intended.

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)
  • 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 gate: signal gate: async
2.0.0-rc.13 runs at once runs at once
2.0.0-rc.14 … 2.0.0-rc.19, next@17d4d1b runs at once deferred until the inner data resolves

Bisect. I built packages/signals from source between the rc.13 and rc.14 release commits, with solid-js and @solidjs/web 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 #4302 ("mounts under one boundary keep their own holds across flushes") fixes some related cases, but not this one.

Docs. The Loading reference says the boundary "renders a fallback while pending async reads inside the subtree settle", and the boundaries guide says that "nested boundaries let panels appear as their own first data arrives". Neither says that the way the outer <Show> was opened should change when render effects inside a nested boundary run.

Activity

  1. ryansolid commented on Oct 28, 2026

    @ryansolid
    Member

    Fixed on next by #4353 (425a713); 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