Dependency leakage between HTML attributes (=Extra dependencies causing unnecessary re-runs)
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
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.
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.
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.
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)