Skip to content

[2.0 rc.15 – rc.19 / next] createOptimisticStore: a reorder inside an action is visible to direct reads but not to <For> when rows have no id #4344

Closed

Description

@neokofg

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

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

  1. ryansolid commented on Oct 28, 2026

    @ryansolid
    Member

    Fixed on next by #4360 (1e115b8); 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