classList causes unnecessary re-runs (but createEffect or HTML text in the same spot doesn't)

I found myself in a situation where classList specifically seems to cause unnecessary re-runs. In the following code, createEffect only reruns when fst() changes (I manually examined w.fsts). Similarly, HTML text re-runs only when fst() changes (I only get one render for ${i} for the element, which was updated. Yet, every single change of any element (executionState()[i]) I get classList for ${i} for every single of my tests(). I don't understand why, especially considering that classList and HTML renders are right next to each other.
<Index each={tests()}>{(t, i) => {
console.log('Test render:', i);
const fst = createMemo(() => executionState()[i]);

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

return <div class="test" classList={{
'passed': (console.log(`classList for ${i}`), fst()?.type === Passed),
}}>
{(console.log(`render for ${i}`), fst(), 'z')}
...
<Index each={tests()}>{(t, i) => {
console.log('Test render:', i);
const fst = createMemo(() => executionState()[i]);

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

return <div class="test" classList={{
'passed': (console.log(`classList for ${i}`), fst()?.type === Passed),
}}>
{(console.log(`render for ${i}`), fst(), 'z')}
...
10 Replies
Igor Konyakhin
Igor KonyakhinOP4w ago
I assembled a self-contained example trying to recreate the issue, but I didn't succeed:
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 class="some" 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>
{values().join(',')}
</div>
}}</Index>;
}
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 class="some" 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>
{values().join(',')}
</div>
}}</Index>;
}
runs exactly as expected without any extra re-runs for classList. I was able to get the minimal example to work: {values().join(',')} {values()[i]} doesn't not cause trouble, but adding nearby: <button disabled={values()[i] <= 2}>Low</button> does
bigmistqke
bigmistqke4w ago
There are some oddities currenly with classList, but afaik they are all about how class and classList are merged with each other. Maybe it has something to do with how some effects are merged in solidjs.
<button disabled={values()[i] <= 2}>Low</button> does
aha, this seems to point towards merged effects solid sometimes goes for less-then-granular as an optimization technique
Igor Konyakhin
Igor KonyakhinOP4w ago
I read about it, but the official page said it's ok to have class static. Regardless, I tried without that extra class attribute and it doesn't change anything
Igor Konyakhin
Igor KonyakhinOP4w ago
GitHub
Dependency leakage between HTML attributes (=Extra dependencies cau...
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)....
mdynnl
mdynnl4w ago
yes, this is a quirk of effect grouping and intentional trade-off for lower creation cost although the expression is re-evaluated, dom won't be updated unless the result actually changes to clarify more, this happens with every attribute that isn't text or children
Igor Konyakhin
Igor KonyakhinOP4w ago
Thanks for a reply!
dom won't be updated unless the result actually changes
Yeah, but I have an array with 200-500 elements, so even if DOM doesn't update, every single attribute of every single array-element re-running for change of any element is undesirable. Now, I think I know how I can fix my specific usecase: replacing disabled={values()[i] <= 2} with cached disabled={v() <= 2} solves it. But, any docs, which describe such SolidJS intricacies? Because this going against the core "fine-grained reactivity" promise. It seems completely undocumented and LLMs couldn't solve this issue. I spent 2+ hours on this and would like to understand the general principle/quirks not to waste time in the future.
bigmistqke
bigmistqke4w ago
Yeah, but I have an array with 200-500 elements, so even if DOM doesn't update, every single attribute of every single array-element re-running for change of any element is undesirable.
Are you noticing performance issues?
Because this going against the core "fine-grained reactivity" promise.
Yes, I understand it might feel a bit counterintuitive. but engineering is about making trade offs and effects have some overhead, so the effects from jsx are often grouped together as a performance optimization. I don't think the effect-grouping is mentioned in the docs. The docs are more focussed on the happy path and don't include all the implementation details.
Igor Konyakhin
Igor KonyakhinOP4w ago
Are you noticing performance issues?
No, I didn't notice a thing even with all the re-runs, but then I have a powerful laptop. I was able to get-rid of classList re-runs, but reactivity is a core SolidJS concept, so it's important to understand its issues/pitfalls.
but engineering is about making trade offs and effects have some overhead, so the effects from jsx are often grouped together as a performance optimization.
I see. In case this trade-off is intentional, I think it would be worth to document it. In general, a page with all the pitfalls/issues/workarounds/patterns/anti-patterns would be super helpful. Thanks for confirming
bigmistqke
bigmistqke3w ago
You are very welcome! There is some talks behind the scenes to put this (and some other frequently asked implementation details) in the docs btw. Apparently it is a question that pops up more frequently
Igor Konyakhin
Igor KonyakhinOP3w ago
That's great! I'd suggest besides documenting implementation details/intricacies, to also make a page with pitfalls: various things that make total sense, but are not obvious at the beginning. For example, even the fact that when you access values()[i] on every list element, it causes every element to re-render on any other element's change. With hindsight, it's logical: since values() changed, everything that depends on values() will re-render. The fact that values()[i] specifically doesn't change is not known by the framework ... and that is why createMemo is useful. I fixed it in my code, but it was not obvious from the start. (Yeah, I know there are also "stores", but I mean generally: in order to understand how to properly use a framework, it's important to understand "why" and for that it's helpful to understand bad/wrong approaches)

Did you find this page helpful?