Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/verdict-mount-unread-pending-chain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/signals": patch
---

A `<Loading>` that a verdict reader (`<Show when={latest(x) ?? x()}>`) mounts reveals its content when a component inside it writes a signal during its mount and a memo that stays pending with no reader reads it through other memos (`gate = createMemo(() => version())`, then a memo over `gate`), at any depth: that memo holds nothing and reaches no boundary. #4203's fix covered only a memo reading the written signal directly (#4340).
10 changes: 7 additions & 3 deletions packages/signals/src/core/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -582,11 +582,15 @@ export function recompute(el: Computed<any>, create: boolean = false): void {
missedWake = (el._flags & REACTIVE_MISSED_WAKE) !== 0;
// A mount's staged read joins it — except a nested derivation left
// pending: no render effect reaches it yet, so it holds nothing and
// reaches no boundary (Reader, #4203). A reader that does is caught.
// reaches no boundary (Reader, #4203), whatever it read — a memo the
// mount bore held over the staging included, at any depth (#4340). A
// reader that does is caught.
joined = !!(
el._flags &
(mount && (prevLane || isEffect || !(el._statusFlags & STATUS_PENDING))
? REACTIVE_JOINED | REACTIVE_STAGED_READ
(mount
? prevLane || isEffect || !(el._statusFlags & STATUS_PENDING)
? REACTIVE_JOINED | REACTIVE_STAGED_READ
: 0
: REACTIVE_JOINED)
);
// A verdict reader is one for as long as it probes, or reads a memo
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* #4340: #4203's fix kept a nested first pass left pending from joining a
* verdict mount's staged read, but only when it read the staging itself. A
* memo that read it through another memo (`gate`) still joined through that
* memo and held the boundary forever, though nothing reads it. A pending
* value no render effect reaches holds nothing and reaches no boundary
* (Reader), at any depth. A render effect that does read it is still held.
*/
import { describe, expect, it } from "vitest";
import {
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();
}
};

function mount(content: () => unknown) {
let slot: unknown;
createRoot(() => {
const value = createMemo(async () => "x");
const outer = createLoadingBoundary(
() => {
// <Show when={latest(value) ?? value()} keyed>
const when = createMemo(() => latest(value) ?? value());
const children = createMemo(() =>
when() ? untrack(() => createLoadingBoundary(content, () => "INNER LOADING")) : undefined
);
return () => {
const c = children();
return typeof c === "function" ? c() : c;
};
},
() => "OUTER LOADING"
);
createRenderEffect(
() => {
let v: any = outer();
while (typeof v === "function") v = v();
return v;
},
v => {
slot = v;
}
);
});
return () => slot;
}

describe("#4340: an unread pending memo behind memos does not hold a verdict-mounted boundary", () => {
for (const depth of [1, 2, 3]) {
it(`reveals with ${depth} intermediate memo(s)`, async () => {
const slot = mount(() => {
const [version, setVersion] = createSignal(0, { ownedWrite: true });
setVersion(1);
let gate: () => unknown = version;
for (let i = 0; i < depth; i++) {
const prev = gate;
gate = createMemo(() => prev());
}
// Never read; never settles.
createMemo(() => (gate(), new Promise(() => {})));
return "CHILD";
});
flush();
await settle();
expect(slot()).toBe("CHILD");
});
}

it("a render effect that reads the pending memo still holds the boundary", async () => {
const slot = mount(() => {
const [version, setVersion] = createSignal(0, { ownedWrite: true });
setVersion(1);
const gate = createMemo(() => version());
const pending = createMemo(() => (gate(), new Promise(() => {})));
createRenderEffect(pending, () => {});
return "CHILD";
});
flush();
await settle();
expect(slot()).toBe("INNER LOADING");
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* @jsxImportSource @solidjs/web
* @vitest-environment jsdom
*
* #4340: #4203 with a memo between the written signal and the pending memo.
* A `<Loading>` that a keyed `<Show when={latest(value) ?? value()}>`
* mounts reveals its content when a component inside it writes an
* `ownedWrite` signal during its mount, derives `gate` from it, and creates
* a memo over `gate` that never settles. Nothing reads that memo: a pending
* value no render effect reaches holds nothing and reaches no boundary.
*/
import { expect, test } from "vitest";
import { Loading, Show, createMemo, createSignal, flush, latest } from "solid-js";
import { render } from "../src/index.js";

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

test("an unread pending memo behind a memo does not hold a latest()-mounted Loading (#4340)", async () => {
const root = document.createElement("div");
function Child() {
const [version, setVersion] = createSignal(0, { ownedWrite: true });
setVersion(1);
const gate = createMemo(() => version());
createMemo(() => (gate(), new Promise(() => {})));
return <p>child</p>;
}

const dispose = render(() => {
const value = createMemo(async () => "v");
return (
<Loading fallback={<p>outer</p>}>
<Show when={latest(value) ?? value()} keyed>
{_value => (
<Loading fallback={<p>inner</p>}>
<Child />
</Loading>
)}
</Show>
</Loading>
);
}, root);
await settle();
expect(root.textContent).toBe("child");
dispose();
});
Loading