Repository navigation
Closed
Description
Describe the bug
EffectOptions.defer is documented as "When true, defers the initial effect execution until the next change", and the reference says "defer: true skips the effect's first run".
An effect with defer: true created inside a component that is mounted by a held update (an async memo above it resolves later) runs its effect function once when the hold commits, although its source never changed. Mounted without a hold, it does not run.
- rc.13:
[]in both cases - rc.20 / next@215c365:
[]without hold,[[1]]under hold
Steps to Reproduce
import { render } from "@solidjs/web";
import { createEffect, createMemo, createSignal, flush, Loading, Show } from "solid-js";
import { expect, it } from "vitest";
const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
async function mountChild(held: boolean): Promise<{ text: string; runs: unknown[] }> {
const runs: unknown[] = [];
const [path, setPath] = createSignal("a");
const matched = createMemo(() => {
const p = path();
return p === "a" || !held ? p : wait(10).then(() => p);
});
function Child() {
const [local] = createSignal(1);
const keys = createMemo(() => [local()]);
createEffect(
() => keys(),
(k) => void runs.push(k),
{ defer: true },
);
return <p>B</p>;
}
const el = document.createElement("div");
const dispose = render(
() => (
<Loading fallback="root">
<Show when={matched() === "b"} fallback={<p>A</p>}>
<Child />
</Show>
</Loading>
),
el,
);
setPath("b");
flush();
await wait(80);
flush();
const text = el.textContent ?? "";
dispose();
return { text, runs };
}
it("control: defer skips the first run when the mount is not held", async () => {
expect(await mountChild(false)).toEqual({ text: "B", runs: [] });
});
it("defer skips the first run when the mount is held by an async memo", async () => {
expect(await mountChild(true)).toEqual({ text: "B", runs: [] });
});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
runs stays [] in both cases.
Actual behavior
Under hold: runs === [[1]] — the effect ran with its initial value at commit.
Impact
Code that relies on defer to skip the initial value (e.g. "react to key changes only", "save on edit") fires spuriously on every held mount.
Metadata
Metadata
Assignees
Labels
No labels
Activity
ryansolid commentedon Oct 31, 2026
Fixed on
nextby #4547 (65da5de); it ships in the next RC.Generated by Claude Code
fix(signals): defer skips a held mount's first effect run (#4543) (#4547
fix(signals): effects a guess mounts honor defer and run once per wri…