Repository navigation
Description
Describe the bug
Since 2.0.0-rc.15, reordering a plain createOptimisticStore array inside an action (setter returning a new array) shows up in direct store reads but not in <For>. While the action is pending, items.map(r => r.name) already gives c,b,a, but <For> (keyed or not) keeps rendering a,b,c. This happens only when the rows have no id field. With an id on every row, both <For> modes show the new order at once.
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 { action, createOptimisticStore, flush, For } from "solid-js";
import { expect, it } from "vitest";
const tick = () => new Promise((r) => setTimeout(r, 0));
for (const withId of [false, true])
for (const keyed of [true, false])
it(`optimistic reorder inside an action reaches <For> (keyed: ${keyed}, rows have id: ${withId})`, async () => {
const row = (n) => (withId ? { id: n, name: n } : { name: n });
const el = document.createElement("div");
let release;
const server = new Promise((r) => (release = r));
let items, reorder;
render(() => {
const [store, setItems] = createOptimisticStore([row("a"), row("b"), row("c")]);
items = store;
reorder = action(function* () {
setItems(() => [row("c"), row("b"), row("a")]);
yield server;
});
return <ul><For each={store} keyed={keyed}>{(r) => <li>{(keyed ? r : r()).name}</li>}</For></ul>;
}, el);
const rows = () => [...el.querySelectorAll("li")].map((li) => li.textContent).join(",");
expect(rows()).toBe("a,b,c");
const done = reorder();
flush(); await tick(); flush();
expect(items.map((r) => r.name).join(",")).toBe("c,b,a"); // store already reordered
expect(rows()).toBe("c,b,a"); // DOM is not, when rows have no `id`
release();
await done;
});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
While the action is pending, <For> renders c,b,a, matching what the store returns when read directly. That is what rc.13 and rc.14 do, and what rc.19 does when the rows carry an id.
Actual on rc.15 – rc.19 and on next@17d4d1b, for rows without id (keyed: true and keyed: false): the DOM stays a,b,c while items.map(...) is c,b,a.
Screenshots or Videos
No response
Platform
- OS: macOS
- Runtime: Node 25.8.2, vitest 3.2.4, jsdom 26.1.0,
@solidjs/babel-plugin(generatedom) - Version:
solid-js/@solidjs/web/@solidjs/signals2.0.0-rc.19. Also reproduces onnext@17d4d1b(signals built from source, which includes fix(signals): optimistic store guesses survive re-asks, answer latest() and stay per-action (#4329) #4330).
Additional context
| solid-js / @solidjs/web / @solidjs/signals | rows without id |
rows with id |
|---|---|---|
| 2.0.0-rc.13, 2.0.0-rc.14 | c,b,a |
c,b,a |
2.0.0-rc.15 … 2.0.0-rc.19, next@17d4d1b |
a,b,c |
c,b,a |
Bisect. I built packages/signals from source between the rc.14 and rc.15 release commits (c421e199fa … ea4438b1a5), with solid-js and @solidjs/web at rc.15:
- first bad: 1c14992
fix(signals): an external source echo satisfies until after await (#3898) (#3937); - last good signals commit:
f6905dc8f7.
Usage. The createOptimisticStore reference shows exactly this return form inside an action (setTodos((t) => t.filter((x) => x.id !== id))). The stores guide says a returned array is written by index: "The callback may return a replacement value instead of mutating. For an array, Solid writes the returned entries by index and adjusts the length." The plain form takes no key option, so nothing documents that an id field is required.
Activity
fix(signals): an optimistic guess's replaced row reaches <For> (#4360)
ryansolid commentedon Oct 28, 2026
Fixed on
nextby #4360 (1e115b8); it ships in the next RC.Generated by Claude Code