T
TanStack•3mo ago
stormy-gold

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:
return useMutation({
mutationFn: (id) =>
service.updateEntity(id),
onSuccess: (updatedEntity) => {
queryClient.setQueryData(
keys.entity(updatedEntity.id),
updatedEntity,
);
},
});
return useMutation({
mutationFn: (id) =>
service.updateEntity(id),
onSuccess: (updatedEntity) => {
queryClient.setQueryData(
keys.entity(updatedEntity.id),
updatedEntity,
);
},
});
service.updateEntity calls and API and maps the result into a new object:
const mapToEntity = ({
id,
activities,
}: EntityAPI): Entity => {
return {
id: mapToIdentifier(id),
activities: activities.map(mapToActivity)
};
};

const mapToActivity = ({
name,
category,
timepoints,
}: ActivityAPI): Activity => {
return {
name,
category,
timepoints: timepoints.map(mapToTimepoint),
};
};

const mapToTimepoint = ({ name, active }: TimepointAPI): Timepoint => {
return {
name,
status: active
? timepointStatusSchema.enum.active
: timepointStatusSchema.enum.inactive,
};
};
const mapToEntity = ({
id,
activities,
}: EntityAPI): Entity => {
return {
id: mapToIdentifier(id),
activities: activities.map(mapToActivity)
};
};

const mapToActivity = ({
name,
category,
timepoints,
}: ActivityAPI): Activity => {
return {
name,
category,
timepoints: timepoints.map(mapToTimepoint),
};
};

const mapToTimepoint = ({ name, active }: TimepointAPI): Timepoint => {
return {
name,
status: active
? timepointStatusSchema.enum.active
: timepointStatusSchema.enum.inactive,
};
};
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.
useMemo(() => {
console.log('>>> entity.activities useMemo triggered');
}, [entity.activities]);
useMemo(() => {
console.log('>>> entity.activities useMemo triggered');
}, [entity.activities]);
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 works
4 Replies
metropolitan-bronze
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:
// what you currently have
const query = useQuery({
...opts,
structuralSharing: false
});

// will work since the activities array should change reference
const derived = useMemo(() => {
// so derivation here
}, [query.data.activities]);
// what you currently have
const query = useQuery({
...opts,
structuralSharing: false
});

// will work since the activities array should change reference
const derived = useMemo(() => {
// so derivation here
}, [query.data.activities]);
With a select function
const query = useQuery({
...opts,
select: useCallback((data) => {
// derive state here
}, [])
})
const query = useQuery({
...opts,
select: useCallback((data) => {
// derive state here
}, [])
})
stormy-gold
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
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-gold
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?

Did you find this page helpful?