solid icon indicating copy to clipboard operation
solid copied to clipboard

Dependency leakage between HTML attributes (=Extra dependencies causing unnecessary re-runs)

Open nns2009 opened this issue 4 months ago • 4 comments

Describe the bug

I found that classList (and possibly other HTML attributes) causes unnecessary re-runs. In the following code, createEffect only reruns when v() changes (I manually examined w.vs). Similarly, HTML text re-runs only when v() changes (I only get one render for ${i} for the element, which was updated. Yet, every single change of any element (values()[i]) I get classList for ${i} for every single element in values(). This is totally unexpected, especially considering that classList and HTML renders are right next to each other.

import { createEffect, createMemo, createSignal, Index } from "solid-js";

export default function ClassListRerunExperiment() {
	const names = createMemo(() => ['one', 'two', 'three', 'four', 'five', 'six', 'seven']);
	const [values, setValues] = createSignal([1, 2, 3, 4, 5, 6, 7]);

	function setOneValue(i: number, v: number) {
		const next = [...values()];
		next[i] = v;
		setValues(next);
	}

	return <Index each={names()}>{(t, i) => {
		console.log('Row render:', i);
		const v = createMemo(() => values()[i]);

		createEffect(() => {
			if (i !== 0) return;
			const w = window as any;
			w.vs = w.vs ?? [];
			w.vs.push(v());
		});

		return <div classList={{
			'big': (console.log(`classList for ${i}`), v() >= 5),
		}}>
			{(console.log(`render for ${i}`), v(), 'z')}
			<button onClick={() => setOneValue(i, v() - 1)}>-</button>
			{v()}
			<button onClick={() => setOneValue(i, v() + 1)}>+</button>
			<button disabled={values()[i] <= 2}>Low</button>
			{values().join(',')} {values()[i]}
		</div>
	}}</Index>;
}

I was able to track down the problem to this specific line:

<button disabled={values()[i] <= 2}>Low</button>

With this specific line removed, reactivity works as expected, any '-'/'+' press only results in a single classList for ${i} log. Somehow, using values() in 'disabled' attribute (maybe also any other HTML attribute?) seems to leak values() as a dependency of classList. At the same time, using values() for rendering:

{values().join(',')}

does not cause any leakage.

Your Example Website or App

https://stackblitz.com/edit/solidjs-dependency-leakage-zqkzgank?file=src%2FApp.tsx

Steps to Reproduce the Bug or Issue

Open the link, open DevTools console. Click any of the '-' or '+' buttons.

Expected behavior

Only one classList for ${i} is logged exactly for the element being updated

Screenshots or Videos

No response

Platform

  • OS: Windows, also Stackblitz, most likely cross-platform
  • Browser: Edge, Opera, Yandex Browser
  • Version: All latest

Additional context

No response

nns2009 avatar Aug 10 '25 00:08 nns2009

This isn't a bug; signal() can't just fetch a part of a nested internal structure. If you want a responsive element for more complex structures, createStore will help.

In the example createMemo, when binding to readSignal, I suspect that the memo is referring to the address with the final destination value, not the array itself, so it should work as you intended.

dennev avatar Aug 10 '25 03:08 dennev

Thanks for the reply. I was explained in the SolidJS Discord that this attribute behavior is a deliberate trade-off made for optimization. Still, if that is the case, it goes against the core "fine-grained reactivity" promise, so I think it would be worth to document/explain it. In general, a page with all the pitfalls/issues/workarounds/patterns/anti-patterns would be super helpful.

nns2009 avatar Aug 11 '25 02:08 nns2009

Thanks, this is duplicate of https://github.com/solidjs/solid/issues/2142

I have asked where to document this and similar behaviours, and are in the process of doing so. Here is a condensed explanation

Solid uses 1 effect to update at the same time, all of the props/atttributes of native elements contained in a component. Everything is cached in local vars and compared with the previous value. The assumption is that props/attrs are deterministic, and only change because some tracked value changed. Therefore, functions that aren't deterministic should be memoized, and non-deterministic objects should be hoisted, to avoid values unexpectedly being changed.

titoBouzout avatar Aug 12 '25 14:08 titoBouzout

Nice to hear it will get documented! I think one mid-large page with all Solid quirks should be possible. There are hopefully limited number of those, but the information on even basic topics so far is very lacking. I just spent 6+ hours on "computations created outside a createRoot or render will never be disposed" 😵 (and couldn't solve it)

nns2009 avatar Aug 14 '25 00:08 nns2009