T
TanStack•2y ago
mute-gold

How to handle nested objects without re-render the whole app?

App example: user has columns, columns has todos, each todo has a done checkbox that can be toggled on and off, basic stuff How should I manage the requests to this use-case? One request to fetch all data? or split the data fetch into individual requests for each column? At some point, the entry data will have to be refetched/mutated/optimistic updated, and this will change the entry data reference and trigger a re-render to my whole app. If I toggle one todo at column #85, all columns and all todos will be re-rendered. Is there a way to subscribe a column/todo to his slice of that big nested entry data object only? I'm rendering the columns like this user.columns.map(column => <Column key column={column} />), so the column knows which slice of the big nested entry data object his data belongs, and must not be re-rendered if his slice didn't changed. The solution I came up with is to use React.memo, deep comparing the column prop to check if his slice have changed in these re-renders. This leads to only the <Column /> with the changed column props to be re-rendered. Is it right? I feel like I'm missing something here. I would like to put this single pieces of data together in one single root source, is easy to work this way. A addNewColumn() method must have a validation user.columns.getAmount() < user.plan.maxColumnsAllowed. Creating these front-end business rules with all data in one source is a super easy. Yes, I know re-render is not a bad thing but in my real use-case, I have a lot of "columns", each "column" have a lot of "todos", each "todo" has their own universe, their data, their modals, their dropdowns, their hooks... Re-render the whole list everytime a small change happened... If this is not a performance concern, I don't know what should be. I put together this small POC. https://github.com/vitormarkis/37react-query-best-practices-nested-objects You can see in the GIFs, the behavior with and without React memo.
GitHub
GitHub - vitormarkis/37react-query-best-practices-nested-objects: 3...
37react-query-best-practices-nested-objects. Contribute to vitormarkis/37react-query-best-practices-nested-objects development by creating an account on GitHub.
No description
No description
11 Replies
sunny-green
sunny-green•2y ago
React Query Render Optimizations
An advanced guide to minimize component re-renderings when using React Query
sunny-green
sunny-green•2y ago
The select property is your friend
mute-gold
mute-goldOP•2y ago
const { data: column } = useQuery({
queryKey: ECacheKeys.user(userId),
select: React.useCallback(user => {
return user.columns.find(c => c.id === columnId)!
}, []),
})
const { data: column } = useQuery({
queryKey: ECacheKeys.user(userId),
select: React.useCallback(user => {
return user.columns.find(c => c.id === columnId)!
}, []),
})
It didn't work, it has the same result as the bad gif I send earlier. It seems like I can't have anything related to the user variable inside the Column component, otherwise it will add user as one of the component dependencies and re-render if the user value change I see tk todo talking about partial subscriptions, but all his examples create the subscriptions of primitive values, like todos => todos.length, not nested objects
eastern-cyan
eastern-cyan•2y ago
It can create subscriptions to nested objects too. It should work. If it doesn't, show a codesandbox please
mute-gold
mute-goldOP•2y ago
https://codesandbox.io/p/github/vitormarkis/37react-query-best-practices-nested-objects/main toggle an todo and check the profiler, expect to re-render only the column/todo affected, got the whole app re-rendering @TkDodo 🔮
eastern-cyan
eastern-cyan•2y ago
@vitor markis 🎈 well in ColumnList, you subscribe to the whole cache entry with:
const { data: user } = useUserQuery({ userId })
const { data: user } = useUserQuery({ userId })
and that triggers a top-down render of all Column components. And since Column isn't memoized, it will re-render. Since you only need the ids here, if you were to only select the ids, you wouldn't get a re-render because the ids didn't change. here's a fixed version: https://codesandbox.io/p/github/vitormarkis/37react-query-best-practices-nested-objects/csb-dwkrtx/draft/condescending-mccarthy in case this doesn't work, here's the ColumnList component:
export const ColumnList = React.forwardRef<React.ElementRef<"div">, ColumnListProps>(
function ColumnListComponent({ className, ...props }, ref) {
const { data: ids } = useUserQuery(
{ userId },
{
select: user => user.columns.map(c => c.id)!,
},
)

if (!ids) {
return <div>Loading...</div>
}

return (
<div
{...props}
className={cn("flex flex-wrap gap-8 ", className)}
ref={ref}
>
{ids.map(id => (
<Column
columnId={id}
key={id}
/>
))}
</div>
)
},
)
export const ColumnList = React.forwardRef<React.ElementRef<"div">, ColumnListProps>(
function ColumnListComponent({ className, ...props }, ref) {
const { data: ids } = useUserQuery(
{ userId },
{
select: user => user.columns.map(c => c.id)!,
},
)

if (!ids) {
return <div>Loading...</div>
}

return (
<div
{...props}
className={cn("flex flex-wrap gap-8 ", className)}
ref={ref}
>
{ids.map(id => (
<Column
columnId={id}
key={id}
/>
))}
</div>
)
},
)
mute-gold
mute-goldOP•2y ago
It worked! ColumnList now adds an array of ids as one of it's dependencies or adds each id string as it's dependencies to re-render?
eastern-cyan
eastern-cyan•2y ago
ColumnList is only subscribed to a list of ids, and that list never changes, so it doesn't re-render
mute-gold
mute-goldOP•2y ago
got you, but deleting or adding new column is causing the whole list to re-render since this array of IDs is changing
eastern-cyan
eastern-cyan•2y ago
sure, it has to. a column could've been added in the middle
mute-gold
mute-goldOP•2y ago
nice, thank you

Did you find this page helpful?