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?4 Replies
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.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
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:
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
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, askI 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
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 its hard to overlap functionality needed by both tables while keeping performance in mind when we restrict ourselves to single component.@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!