setQueryData & nested arrays immutability
I am experiencing an issue with useMemo in my react app. Basically, it's NOT getting triggered when its dependency is updated.
Simplified code example:
service.updateEntity
calls and API and maps the result into a new object:
As you can see entity contains a nested array: activities > timepoints
. Even though I think I am mapping correctly, not mutating any existing state (I think), my useMemo which has a dependency on entity.activities
is not getting triggered when I update the status
of a nested timepoint. I can see the server returning the updated state.
In the ui.dev
course I remember react query having some smart caching mechanism: "structural sharing", could this be causing this issue?
Notes
- 1) if I update the mapping to always append some random UUID to the name, it's seems like useMemo is triggering
- 2) If I disable structuralSharing on query level the useMemo works4 Replies
metropolitan-bronze•2mo ago
useMemo
purely relies on Object.is
comparisons - that is, a referential check to see if the object has changed reference - to determine whether the dependencies have changed.
Since Tanstack query uses structural sharing (essentially, only creating new references for things that have actually structurally changed), it ensures that the reference to your array should remain the same, even though you may have recreated it and added a new item.
So ultimately this will be the reason that your useMemo is not re-running, is because the array reference is going to remain the same reference. With this said, for your first point, I don't see why this would cause your useMemo to re-run. In the pseudo code you provided, this does not make sense because the array reference should remain the same, despite elements inside the array changing, so I think you would need to provide a stack blitz reproduction that reflects this behavior wholistically.
---
It depends on your use case, but if you want to derive some state directly from a query with no other dependencies, this is what the select
function is for. It allows you to derive some data from a query before returning to the component, which in turn will avoid re-renders when nothing about your derived state has changed.
i.e.
Currently what you have:
With a select function
stormy-goldOP•2mo ago
First of all, thanks for your response!
"I don't see why this would cause your useMemo to re-run. In the pseudo code you provided" -> It was the name on activity level, not on timepoint level
"this is what the select function is for" -> does this mean that by using select I can enable sturcturalSharing once again?
extended-salmon•2mo ago
If anything in the array changes, steucturalSharing will give you a new top level array too
If you get the same array, it means nothing inside of it changed
So useMemo should re-run "if necessary", that's the whole point of structuralSharing
stormy-goldOP•2mo ago
@TkDodo 🔮
"If anything in the array changes, steucturalSharing will give you a new top level array too"
But if an array contains objects, and lets say a property of the second object in the array changed from true to false. It seems like structuralSharing will reuse that same array reference or am I just confused.
Also, on https://tkdodo.eu/blog/react-query-render-optimizations I've read:
"As I've hinted before, for selectors, structural sharing will be done twice: Once on the result returned from the queryFn to determine if anything changed at all, and then once more on the result of the selector function"
Let's assume the main
queryFn
returned an object containing an array called activities
, it could be that the structural sharing on the main level detected a change, for example lastModified
got updated (on the same level of activities
). Even tough this change was detected a select
on activities
could be blocked by the second structural sharing check eventhough one of the items within the activities
array got updated as the array reference was reused. Is that how I should understand that statement?