Skip to content

Object class values do not update after an in-place mutation with equals: false #3188

Description

@nickshiro

Describe the bug

In Solid 2, an object passed to the class prop does not update the DOM when that object is mutated in place and its signal uses { equals: false }.

The signal correctly notifies its dependents, but the class runtime compares the new value against the same already-mutated object. Consequently, previously applied classes are not removed and newly enabled classes are not added.

style object values already handle this case by keeping a separate snapshot of the declarations applied to the DOM. The object form of class does not appear to keep an equivalent snapshot.

Steps to Reproduce the Bug or Issue

  1. Render the example.
  2. Inspect the target element. It initially has class="before".
  3. Click Update classes.
  4. Inspect the element again.

The following regression test also demonstrates the issue:

  test("removes classes after an in-place object update", () => {
    const classes = { before: true, after: false };
    let div!: HTMLDivElement;
    let notify!: () => void;

    createRoot(() => {
      const [value, setValue] = createSignal(classes, { equals: false });
      notify = () => setValue(classes);
      div = <div class={value()} /> as HTMLDivElement;
    });

    expect(div.className).toBe("before");

    classes.before = false;
    classes.after = true;
    notify();
    flush();

    expect(div.className).toBe("after");
  });

The final assertion currently fails:

Expected: "after"
Received: "before"

Expected behavior

After the signal notification, the target element should have:

<div id="target" class="after">Target</div>

The before class should be removed and the after class should be added.

equals: false is documented as always notifying dependents even when the signal value retains the same identity, so DOM bindings should process the updated object contents.

Platform

  • Solid: next at f051db6
  • Test environment: Vitest with jsdom
  • OS: Linux

Additional context

className() receives the same object for both value and prev:

prev = classListToObject(prev || {});
value = classListToObject(value);

Because the object was already mutated before the signal notification, both normalized values contain the new state. The runtime therefore cannot determine which classes were previously applied to the DOM.

The nearby style() implementation avoids the same problem by storing a separate snapshot on the element:

let applied = node._$styles;

A similar per-element snapshot of classes applied by className() could allow in-place object updates to be diffed without removing unrelated classes.

Related but different: #2673 reported stale dependencies for an object nested inside a class array. That issue was closed as a dependency/build problem; this reproduction consistently reaches the class update
but loses the previous object state.

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