#3498·solid

Shallow computed stores lose leaf identity; hybrid hydration can lose the first answer

Author: MonkeylordzCreated Sep 16, 2026Updated Sep 16, 2026

Describe the bug

On Solid 2 next (59c4192a, package version 2.0.0-rc.8), computed stores have two related correctness problems in their draft/snapshot and hydration paths. Both affect ordinary and optimistic stores.

Shallow projections stop being shallow

With { shallow: true }, nested values should remain raw references. However:

  • The client projection draft wraps nested objects in another proxy, so reading a leaf from the draft does not preserve its identity.
  • Loading shadows, SSR snapshots, and hydration replay shadows use JSON.parse(JSON.stringify(...)) even for shallow stores. This copies shared references, turns Dates into strings and NaN into null, drops undefined properties, and cannot represent BigInts or cycles.
  • The SSR draft also deep-proxies shallow leaves. Accessing an object-valued, non-configurable property on a frozen leaf can throw a Proxy invariant error.

These transformations happen inside store handling, before the normal serialization protocol can preserve supported values.

Hybrid store hydration can lose an answer

The hybrid store path flips its hydration gate immediately after construction. That can supersede the server-backed computation before both the first server answer and the DOM claim are complete. With seedLoadingValue: true, a delayed server answer can be lost and the store stays at its seed.

The same path also suppresses the first yield of every client generator run. Only the initial handoff duplicates a server answer; suppressing the first yield again after a dependency change can discard a valid update. Completing hydration can also replace an adopted server error without an explicit refresh.

Reproduction

This synchronous example exposes the shallow draft problem without an application or DOM:

typescript
import { createRoot, createStore } from "solid-js";

createRoot(() => {
  const value = Object.freeze({ child: {}, date: new Date(0) });
  createStore(
    draft => {
      console.assert(draft.value === value, "shallow draft changed leaf identity");
      console.assert(draft.value.child === value.child);
    },
    { value },
    { shallow: true }
  );
});

For loading snapshots, use an async compute with { shallow: true, seedLoadingValue: true }, replace the leaf in the draft, then await a deferred promise. After resolution, the replacement should remain the exact object supplied by the compute; it is currently JSON-cloned.

For hybrid hydration:

  1. Server-render an async computed store with seed { count: 0 } and { ssrSource: "hybrid", seedLoadingValue: true }.
  2. Hydrate the loading seed while the serialized first answer is still pending.
  3. Resolve the server answer to { count: 1 }. The store can remain at 0 instead of adopting 1.
  4. With an async generator whose sole yield writes a reactive version(), change that dependency after handoff. The first yield of this new run must be committed rather than discarded as another hydration duplicate.

The proposed fix includes standalone regression tests for object/array roots, ordinary/optimistic stores, shallow loading/SSR snapshots, hybrid loading and subsequent updates, and server-error recovery. Against the unmodified next sources, 18 of those 20 cases fail.

Expected behavior

  • Shallow drafts and snapshots preserve raw leaf references and values. Only the root container is copied where an independent snapshot is required.
  • Hybrid stores adopt the first server answer and finish hydration before starting the live client source.
  • Only the initial client handoff skips its duplicate first yield; subsequent computes commit their first yield normally.
  • An adopted server error remains visible until an explicit refresh, which can obtain a fresh client answer.

Platform

Windows, Node 24.18.0; reproduced with Solid's server and jsdom hydration tests. No browser-specific behavior is required for the shallow-store reproduction.