S
SolidJS2mo ago
QWu4xYV

Reactivity in single table cell from memoized data

[Moving from this thread (https://discord.com/channels/722131463138705510/1241054227485425734/1241622154437726360) since this is a separate question] Question: How do I do some transformation/aggregation on store data before displaying it in a tabular format, when I want only a single cell to update? For example, in https://stackblitz.com/edit/solidjs-templates-inkran?file=src%2FApp.tsx , I aggregate over each of the keys. With a createMemo and some use of prev, I can get just a single row to update, but would ideally just update a single cell. (My understanding is that in this case, because createMemo returns a Signal (not a Store), there isn't a good way to listen for nested values, but I'm not sure about that...) If I have my createMemo update prev in place, it seems that For/Index doesn't know that the update has happened (which makes sense), but if I create a new object for that row, even if a value hasn't changed for that index, it still updates anyway (in this case, the label "B"). The only other thing I can think of is to create a separate store that I then update in separate createComputed or something like that, but that feels a bit awkward... What am I still missing here?
QWu4xYV
StackBlitz
Nested For with Memo - StackBlitz
Vite + solid templates
4 Replies
Dakotys
Dakotys2mo ago
Why doesn't prev[i].total = total; cause update? createMemo returns new list of references, but but those references still point to same place and at the same time you just modified one of the references .total = total, then update in For loop is triggered, for loop compares each object by reference but discovers that they are the same => nothing changes. In case of prev[i] = { ...prev[i], total }; you are reassigning new object at certain index not updating reference. For loop iterates over objects and finds out only one has changed, that means that its not change in order of elements and instead of moving nodes in DOM it recreates changed nodes => it recreates new Index loop that recreates elements inside => new row rerender.
Maciek50322
Maciek503222mo ago
Granularity of updates depends on granularity of your code. You probably know this by now but let's go through that again: - The signal memoizedStore is updated as a whole (because of [...prev]). - Then anyone that subscribed to memoizedStore will have to do some updating computation. - One of them is <For each={props.data}>, it gets the information that props.data has changed and so it starts calculations on what changed inside the array and updates only changed elements with given function. - Then if some element in array has changed, the <For> calls given function for changed element which has <Index each={Object.values(row)}>. - Here the <Index> is recalculated always, because <For> triggered this function and everything inside this function (including <Index> and all of it's insides) has to be recalculated, that means the whole row is rerendered. The <For> can go only so deep in checking what changed and triggering render function for changed element. In your case the changed element is actually different list of elements. That's why whole row updates. For storeData story is bit different: - You update only one property using path setStoreData(1, 'b', (prev) => prev + 1), not the whole array, just single property inside array inside array. - Then anyone that subscribed to storeData[1]["b"] will have to do some updating computation - One of them is {String(val())} directly (inside <For>, inside <Index>). It doesn't seem like it subscribes for any change here, but it actually does implicitly. Because we are in JSX context inside {...}, it creates implicit function call for whole expression, because it sees that the expression uses function call (val()), so it actually is like () => String(val()), and as you know the function that uses signal is signal, that's why when val() updates, the string in this place also updates. Also val() itself is a subscription to this specific property inside store ... that's because stores use proxies / getters when accessing their properties (implicit functions) that automatically subscribe to the property change And that's it, when using store <For> doesn't get triggered, nor does <Index> to do their recalculations and so you get the granular updates Store in a nutshell is a signal that contains other signals and uses getters (implicit functions) to call (get value of) signal. createMemo creates signal. That is most shallow of signals. It can be updated only as a whole and trigger all recalculations that subscribed to this signal as a whole. Unless of course you would subscribe to signal within this signal (stores do this) (more on that later) I want to also explain why this
aggregated.forEach(({ label, total }, i) => {
if (total !== prev[i].total) {
// This doesn't update
//prev[i].total = total;
// But this triggers both to render
prev[i] = { ...prev[i], total };
}
});
aggregated.forEach(({ label, total }, i) => {
if (total !== prev[i].total) {
// This doesn't update
//prev[i].total = total;
// But this triggers both to render
prev[i] = { ...prev[i], total };
}
});
works how it works either way in this context if prev[i] is a signal or store, just assigning prev[i].total = total wouldn't work, because it is not a function that would trigger any subscriber's update, to trigger updates you have the setStore & setSignal (also there are mutable that would work like that, because they have implicit setters, but have some other caveats). By doing prev[i] = { ...prev[i], total } you give new reference at this place, which <For> can detect and trigger update for this element, but first you have to tell the <For> that it should recalculate whole array, which you do by doing [...prev] at the end. So what are other ways to make more granular updates? - as you said createComputed with stores, but yeah you have to know what you are doing - signals inside signals: You can subscribe to particular signal eg. inside <For> and then set this particular signal inside some update function, consider:
function createSignalAsObject(val) {
const [get, set] = createSignal(val);
return { get, set }
}

const [array, setArray] = createSignal([
createSignalAsObject(0),
createSignalAsObject(0),
createSignalAsObject(0),
])

const increment = () => {
array()[1].set(prev => prev + 1)
};

return (
<For each={array()}>
{(item) => <>{item.get()}<>}
</For>
)
function createSignalAsObject(val) {
const [get, set] = createSignal(val);
return { get, set }
}

const [array, setArray] = createSignal([
createSignalAsObject(0),
createSignalAsObject(0),
createSignalAsObject(0),
])

const increment = () => {
array()[1].set(prev => prev + 1)
};

return (
<For each={array()}>
{(item) => <>{item.get()}<>}
</For>
)
It's something you already have but using stores. Here inside <For> render function we directly subscribe to particular signal value within the other signal, now the increment doesn't update whole array, but just this one signal, and so <For> doesn't have to recalculate I'll leave that to you how you can use it to achieve more granularity Also I have another fun fact, you don't actually have to use elements from <For> / Index render functions, depending on what you have this could be more efficient or less
<For each={new Array(array().length)}>
{(_, i) => <>{array()[i()].get()}<>}
</For>
<For each={new Array(array().length)}>
{(_, i) => <>{array()[i()].get()}<>}
</For>
In this case using simplest signals should be less efficient, because when whole array changes, then everything gets recalculated inside <For> render function, because you subscribed to array() inside, and so every element will rerender on array() change. But also it is subscribed to particular signal inside this array ([i].get()), so it will react to this particular signal change. But what about stores? I'll also leave that to you to find some way you could use this (you don't have to) And if you have some questions, ask
Dakotys
Dakotys2mo ago
I agree with Maciek50322 and you have many options how to deal with that problem. Your table component is trying to do too many different things with just one prop. Other solution could be to replace For loop with another Index loop, than there would be less dom recreations
<tbody>
<Index each={props.data}>
{(row) => (
<tr>
<Index each={Object.values(row())}>
{(val) =>
(console.log({
backingData: props.name,
val: val(),
}) as any) || <td >{String(val())}</td>
}
</Index>
</tr>
)}
</Index>
</tbody>
<tbody>
<Index each={props.data}>
{(row) => (
<tr>
<Index each={Object.values(row())}>
{(val) =>
(console.log({
backingData: props.name,
val: val(),
}) as any) || <td >{String(val())}</td>
}
</Index>
</tr>
)}
</Index>
</tbody>
but the signal would still be reevaluated but instead of creating node it would just repeatedly do td.textContent = 'B' on each update. So it is not optimal. Maybe make separate table components that take as input different data structures
this one lists just values, where we expect that each one can change
1 2 3
4 28 6
7 8 9
this one lists label and value but we expect only the value to change
A 12
B 38
C 18
this one lists just values, where we expect that each one can change
1 2 3
4 28 6
7 8 9
this one lists label and value but we expect only the value to change
A 12
B 38
C 18
its hard to overlap functionality needed by both tables while keeping performance in mind when we restrict ourselves to single component.
QWu4xYV
QWu4xYV2mo ago
@Maciek50322 @Dakotys , thanks so much for your detailed responses! While I knew a lot of it, the big thing that helped me unstick myself was the fact that Signals inside Signals (or Store inside Signals, etc) is okay. I stumbled across the fact that I could do this, but wasn't sure if it was a recommended pattern or something I should try to stay away from. With this (and also the encouragement that createComputed could also work), I created a few solutions here: https://stackblitz.com/edit/solidjs-templates-xaiwp8?file=src%2FApp.tsx But all of those compute totals for the whole table, which at least in this case is less efficient. @peerreynders came up with a better solution on the original thread https://discord.com/channels/722131463138705510/1241054227485425734/1241901321171374140 , which takes this into account. Thanks all!