Skip to content

Optimistic writes disappear on rows retained from a derived store's draft #3859

Closed

Description

@GabbeV

Describe the bug

A writable derived store retains a local row across a source refresh:

const [local, setLocal] = createStore<Row[]>(
  draft => [...source(), ...draft.filter(row => row.id === "local")],
  [],
);
const [view, setView] = createOptimisticStore(local);

After the refresh, an action optimistically sets the retained row's value to 1 and waits two seconds. The optimistic view keeps showing 0 throughout the action.

There are no concurrent actions or pending source updates; the setup updates finish before the action starts.

Your Example Website or App

https://s.olid.uk/id/zG2yQHbUQPa-SyUMiPm_pg

Steps to Reproduce the Bug or Issue

  1. Open the playground.
  2. Click Run once. The button creates a local row, refreshes the source, then starts the optimistic action, with 100 ms delays between setup updates.
  3. Observe that Local value stays at 0 during the two-second action.

Expected behavior

The optimistic view should show Local value: 1 while the action is pending, then revert to 0 when the action finishes without persisting the change.

Screenshots or Videos

No response

Platform

  • OS: Linux
  • Browser: Chromium 153.0.8010.12
  • Solid: published preview mizulu/solid2@5c2174c

Additional context

Returning a copy of the retained row (draft.filter(...).map(row => ({ ...row }))) makes the optimistic update appear and revert as expected. Both variants were tested in the browser.

The retained-draft-row pattern is already covered for ordinary writes by the regression tests for #3767. This report concerns optimistic writes over that store.

The playground has Tailwind disabled.

Activity

  1. ryansolid commented on Oct 7, 2026

    @ryansolid
    Member

    Confirmed, and thanks for the minimal pattern. It reproduces on next and with #3855 (which fixes a different case). It needs the re-derivation: without the source refresh, the retained row shows the guess and reverts correctly, and copying the row avoids it, as you found. So the optimistic view most likely still tracks the row from before the refresh. It needs its own fix; we'll pin your repro as a test.

    — Claude via Cursor

  2. ryansolid commented on Oct 7, 2026

    @ryansolid
    Member

    Fixed in #3876 (merged into next as d1309ad). Thanks @GabbeV for the minimal repro and the copy workaround; the workaround pointed straight at the cause.

    A correction to our earlier comment: the optimistic view did track the right row. Its proxy and targets are the same objects before and after the refresh. What changes is the derived store's committed array. Your derive returns the draft wrapper from draft.filter(...), and since the #3767 fix that wrapper is registered as an alias of the row, so it sits in the committed array in place of the row's raw object. Reads through the derived store resolve the alias, which is why everything looked fine.

    The optimistic setter's draft clones that committed array. The wrapper forwards $TARGET, so the chained read took it for a store proxy and skipped resolving it. It then wrapped it as a second row target, and the write became a guess on nodes nothing read. A copied row is a plain object, so it resolved to the real row target, which is why your workaround worked.

    The fix resolves anything that isn't a real store proxy on that path. Your sequence is now a regression test, along with the copied-row variant as a control.

    — Claude via Cursor

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