S
SolidJS6mo ago
finsook

Reactivity not referentially transparent?

I don't understand why CounterList doesn't work here: https://playground.solidjs.com/anonymous/656ba247-562b-4c2b-b32a-bf734e09fd90. Clicking the others works fine but the middle row of buttons doesn't update. At least there is an ESLint warning, but it seems like adding a let shouldn't change anything. It seems to be something with how the compile-time reactivity works. The message mentions "tracked scopes" but I have no idea how they work and there is very little mention in the documentation. Isn't Index a tracked scope?
<Index each={countList()}>
{(count) => {
let c = count();
return <button type="button" onClick={increment}>
{ c.x + " " + c.y }
</button>
}}
</Index>
<Index each={countList()}>
{(count) => {
let c = count();
return <button type="button" onClick={increment}>
{ c.x + " " + c.y }
</button>
}}
</Index>
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
46 Replies
peerreynders
peerreynders6mo ago
What's wrong with For (given that your item is an object, not a primitive value).
function CounterList() {
const [countList, setCountList] = createSignal([
{ x: 1, y: 2 },
{ x: 1, y: 2 },
]);
const increment = () =>
setCountList((countList) =>
countList.map((count) => ({ x: count.x + 1, y: count.y + 1 }))
);

return (
<For each={countList()}>
{(c) => (
<button type="button" onClick={increment}>
{c.x + ' ' + c.y}
</button>
)}
</For>
);
}
function CounterList() {
const [countList, setCountList] = createSignal([
{ x: 1, y: 2 },
{ x: 1, y: 2 },
]);
const increment = () =>
setCountList((countList) =>
countList.map((count) => ({ x: count.x + 1, y: count.y + 1 }))
);

return (
<For each={countList()}>
{(c) => (
<button type="button" onClick={increment}>
{c.x + ' ' + c.y}
</button>
)}
</For>
);
}
finsook
finsookOP6mo ago
Well, I know that the number of items in the list is fixed or at least changes infrequently. Originally I was working with data like [{value: 1, unit: 'hours'}, {value: 1, unit: 'minutes'},{value: 1, unit: 'seconds'},{value: 1, unit: 'milliseconds'}], and there appears to be a small speedup in fps from switching from For to Index. As I understand it, because For is not keyed, it redoes the whole element every update, whereas Index updates only the milliseconds.
peerreynders
peerreynders6mo ago
For is not keyed, it redoes the whole element every update
For is keyed to the object reference. So if it is re-rendering every row, you are replacing the objects in the array. Now sometimes that is unavoidable when the data comes from an external API—using a store and reconcile() with { merge : true } can mitigate that.
finsook
finsookOP6mo ago
oh, it uses object identity or something? I generate the whole array from a timestamp every frame. IDK, I am looking at Svelte and you can just give a value as the key, it seems easier.
peerreynders
peerreynders6mo ago
The Solid counter part is using a store with reconcile() and { key: 'id' } option.
finsook
finsookOP6mo ago
it's similar, but it looks like in Svelte you can write an arbitrary expression for the key, it is not limited to an attribute This isn't using crazy keys, but I think it shows how flexible Svelte is compared to Solid. AFAICT though it is the shortest way to emulate Solid's Index behavior.
{#each Array.from(countList.entries()) as [i,c] (i)}
<button on:click={increment}>
{c.x + " " + c.y}
</button>
{/each}
{#each Array.from(countList.entries()) as [i,c] (i)}
<button on:click={increment}>
{c.x + " " + c.y}
</button>
{/each}
for example instead of destructuring you can use as arr (arr[0])} in the each.
peerreynders
peerreynders6mo ago
For me the fundamental difference is that For moves the entire item DOM fragment in accordance with the associated object reference while Index will only swap text nodes. Is this about wanting to use the (numeric) array index as a key? (The thing that React always tells you not to do).
finsook
finsookOP6mo ago
hmm, so they both use ===: https://github.com/solidjs/solid/blob/c9049dc8ddb39fc2163082c7bde3f507eb1ce831/packages/solid/src/reactive/array.ts. I guess the difference is really that For has a diff algorithm type thing, whereas Index does a simple index-by-index check. In that sense Index does use the numeric index as a key.
peerreynders
peerreynders6mo ago
My guess is that code primarily governs the reactive behaviour. The other aspect is the relationship to the DOM via https://github.com/ryansolid/dom-expressions
GitHub
GitHub - ryansolid/dom-expressions: A Fine-Grained Runtime for Perf...
A Fine-Grained Runtime for Performant DOM Rendering - ryansolid/dom-expressions
finsook
finsookOP6mo ago
peerreynders
peerreynders6mo ago
Index demo: The DOM elements never change because Index just blasts new strings into the text nodes: https://playground.solidjs.com/anonymous/0467cd68-f465-4ed0-8c48-1d5081d2dcbc Plain For with signal: item rows are always deleted and re-rendered. https://playground.solidjs.com/anonymous/49128b6c-ebcf-4820-8d90-0dcbc9f6a8e3 For with store and reconcile: existing item rows are are retained.
https://playground.solidjs.com/anonymous/437026b8-e80a-4b7c-bcfc-74f986be43f1
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
mdynnl
mdynnl6mo ago
peerreynders
peerreynders6mo ago
While you are here… Why does this break?
<Index each={countList()}>
{(count) => {
let c = count();
return (
<button type="button" onClick={increment}>
{c.x + ' ' + c.y}
</button>
);
}}
</Index>
<Index each={countList()}>
{(count) => {
let c = count();
return (
<button type="button" onClick={increment}>
{c.x + ' ' + c.y}
</button>
);
}}
</Index>
mdynnl
mdynnl6mo ago
if the breaking is the expression inside jsx not updating, it's because count() is outside of jsx. eslint plugin is correct here.
peerreynders
peerreynders6mo ago
So that item function isn't inside an effect and by extension each item JSX is a separate effect?
mdynnl
mdynnl6mo ago
yeah, they're mini-components (untracked) including For's
peerreynders
peerreynders6mo ago
Good to know. Thank You!
mdynnl
mdynnl6mo ago
in general, it's safe to assume a render function (children function) of a jsx component to be untracked unless the author intentionally did it of course for some reason
finsook
finsookOP6mo ago
could you make it be inside an effect? like wrap the (count) => { ... } function in a createEffect function?
<Index each={countList()}>
{createEffect((count) => { // doesn't work, createEffect returns void
let c = count();
return (
<button type="button" onClick={increment}>
{c.x + ' ' + c.y}
</button>
);
})}
</Index>
<Index each={countList()}>
{createEffect((count) => { // doesn't work, createEffect returns void
let c = count();
return (
<button type="button" onClick={increment}>
{c.x + ' ' + c.y}
</button>
);
})}
</Index>
mdynnl
mdynnl6mo ago
you want createMemo or a fragment + iife here i assume you're already aware that this would re-create the nodes which solid tries to avoid most of the times and don't really need to if you're using nested signals (manually or with proxy through stores)
<Index each={countList()}>
{(count) => (
<>
{(() => {
let c = count();
return (
<button type="button" onClick={increment}>
{c.x + " " + c.y}
</button>
);
})()}
</>
)}
</Index>
<Index each={countList()}>
{(count) => (
<>
{(() => {
let c = count();
return (
<button type="button" onClick={increment}>
{c.x + " " + c.y}
</button>
);
})()}
</>
)}
</Index>
finsook
finsookOP6mo ago
well in this case countList() is already a signal/memo. I guess it is not too important, it is mainly to write c instead of count() so the JSX is not too long
mdynnl
mdynnl6mo ago
if you don't mind a little bit of overhead, you can use mergeProps(...)
finsook
finsookOP6mo ago
I guess I'm not clear, why is count a signal for Index but a value for For?
mdynnl
mdynnl6mo ago
i'm not a native speaker so my language might be a bit simpler and easier to understand i guess 😆 Index cares about changes at specific indices whereas For cares about the individual items
finsook
finsookOP6mo ago
but, is there a technical reason why Index could not pass values to its children function?
mdynnl
mdynnl6mo ago
i'm not sure i understand the question i guess this already contains the answer?
peerreynders
peerreynders6mo ago
If I had to guess it's because the value is reactively projected into the rendered DOM. In the case of For that is not necessary because the item fragment lives and dies with the reference of the object that was used to create it.
finsook
finsookOP6mo ago
like with Key, everything is a signal. so you have to write item()
<Key each={items()} by={item => item.id} fallback={<div>No items</div>}>
{item => <div>{item().a + item().b}</div>}
</Key>
<Key each={items()} by={item => item.id} fallback={<div>No items</div>}>
{item => <div>{item().a + item().b}</div>}
</Key>
but with for, it is a value, so you can destructure
<For each={items()}>
{{a,b} => <div>{a + b}</div>}
</For>
<For each={items()}>
{{a,b} => <div>{a + b}</div>}
</For>
so it is much more convenient
mdynnl
mdynnl6mo ago
this explains well Index doesn't rerun the function when a value changes, instead it gives you a signal that you use it in the jsx when using solid-js, i guess you probably get use to it when it comes to destructuring
finsook
finsookOP6mo ago
but is there a reason one couldn't make a version of Index that acted like For and reran the function?
mdynnl
mdynnl6mo ago
like this but same api as Index?
peerreynders
peerreynders6mo ago
If you rerun the function you are recreating DOM. For only runs the function for objects it hasn't seen before.
finsook
finsookOP6mo ago
well like Key for example, a version that didn't pass signals
<Key2 each={items()} by={item => item.id} fallback={<div>No items</div>}>
{{a,b} => <div>{a + b}</div>}
</Key>
<Key2 each={items()} by={item => item.id} fallback={<div>No items</div>}>
{{a,b} => <div>{a + b}</div>}
</Key>
As peerreynders says it would probably have to use some sort of caching to avoid rerunning the function too much
mdynnl
mdynnl6mo ago
i think this will eventually get to dom diffing runtime-wise if you're okay with added mental overhead added by compilers, this is already possible https://github.com/orenelbaum/babel-plugin-solid-undestructure https://github.com/lxsmnsyc/solid-labels
peerreynders
peerreynders6mo ago
The syntax suggests to me that it creates a mapping from item.id to the rendered fragment, assuming that the content of the item never changes.
finsook
finsookOP6mo ago
yeah, I think you would also add a parameter for comparing the item against its cached version. Or just render the fragment and compare the fragment with dom-diffing
peerreynders
peerreynders6mo ago
keyed rendering assumes that the rendered item is immutable.
finsook
finsookOP6mo ago
well of course, everything is immutable. but if you change the items list then there is a new item and an old item matched by the id / key
peerreynders
peerreynders6mo ago
Given
{ id: 'abc', a: 1, b: 2 }
{ id: 'abc', a: 1, b: 2 }
then
['abc', <div>1 + 2</div>]
['abc', <div>1 + 2</div>]
That is all it cares about.
finsook
finsookOP6mo ago
Right, but say you have:
{ id: 'abc', a: 1, b: 2 } -> { id: 'abc', a: 1, b: 3 }
{ id: 'abc', a: 1, b: 2 } -> { id: 'abc', a: 1, b: 3 }
Then you can run the render function {id, a, b} => [id, <div>{a}+{b}</div> and get
['abc', <div>1 + 2</div>] -> ['abc', <div>1 + 3</div>]
['abc', <div>1 + 2</div>] -> ['abc', <div>1 + 3</div>]
And then turn that into a DOM text modification using a diff algorithm. And with a Sufficiently Smart Compiler you can skip the diff and compile the render function directly into an incremental DOM update function
function update({id_old, a_old, b_old}, {id_new, a_new, b_new}) {
assert(id_old == id_new);
if (a_old != a_new || b_old != b_new) {
elem.textContent = `${a}+${b}`;
}
}
function update({id_old, a_old, b_old}, {id_new, a_new, b_new}) {
assert(id_old == id_new);
if (a_old != a_new || b_old != b_new) {
elem.textContent = `${a}+${b}`;
}
}
I was hoping the compilers you referred to were these magical incremental update compilers, but it seems they are just minor syntax hacks to make using signals easier
peerreynders
peerreynders6mo ago
Solid uses a compiler for JSX. SolidStart for 'use server' and file routes. Svelte is the one with “magical” compilers (and I guess now React).
peerreynders
peerreynders6mo ago
Simple is better than easy A lesson that comes hard for fine-grained reactivity. Explicit and consistent conventions even if they require more effort are worth it. The aim is to provide minimal tools to serve as the basis to build upon.
https://www.solidjs.com/guides/getting-started#4-simple-is-better-than-easy
SolidJS
Solid is a purely reactive library. It was designed from the ground up with a reactive core. It's influenced by reactive principles developed by previous libraries.
peerreynders
peerreynders6mo ago
Ryan Carniato
YouTube
JavaScript Frameworks - Heading into 2024
It's my first stream of the new year. Let's talk about JavaScript frameworks. Where things have been and where they are going. [0:00} Starting Soon... [2:30] Preamble [8:45] The Plan for Today [17:00] Ryan's Predictions Last Year [27:00] React in 2023 [33:30] React - Server Components with No Server [45:00] React - Next & Server Actions [57:30]...
Ryan Carniato
YouTube
JavaScript Frameworks - Heading into 2024
It's my first stream of the new year. Let's talk about JavaScript frameworks. Where things have been and where they are going. [0:00} Starting Soon... [2:30] Preamble [8:45] The Plan for Today [17:00] Ryan's Predictions Last Year [27:00] React in 2023 [33:30] React - Server Components with No Server [45:00] React - Next & Server Actions [57:30]...
finsook
finsookOP6mo ago
lol alright, I was already thinking about Svelte, now even SolidJS devs are telling me Svelte is easier 😉 I have argued about simplicity with people before and I am firmly in the "you can't build an airplane without a million parts" camp for example the way I started was combining SolidJS and Svelte components in a single app...
bigmistqke
bigmistqke6mo ago
i saw https://fxtwitter.com/youyuxi/status/1800899548778479992 and had to think of this 🧵 . it's all about philosophies in the end. and in accepting that all abstractions leak in some way or another, u just gotta choose which type of leakage u prefer.
💬 2 🔁 1 ❤️ 69 👁️ 13.8K
FxTwitter / FixupX
Evan You (@youyuxi)
@Rafael_Casuso @emil_priver @sveltejs @Rich_Harris Also to many users the compiler-based approach is just too magical and works against standard JS intuition. It works well for Svelte’s audience but not necessarily for others. It’s another trade-off we are intentional about, not because we don’t understand it.
Want results from more Discord servers?
Add your server