T
TanStack•2mo ago
vicious-gold

Only update affected post in infinite feed?

Imagine the following scenario: 1. user visits a page w/ an infinite posts feed 2. user scrolls down for a while, see a post they like, and upvote it 3. only the relevant data updates & only the relevant component re-renders How would you implement this? Image shows an implementation example, but it look terribly inefficient to update the reference to the entire paginated object.
No description
12 Replies
afraid-scarlet
afraid-scarlet•2mo ago
I am in no way shape or form sure if this is best practice, but I have done something similar to this, for a more niche scenario where it made more sense, but here goes: In the infinite query, add logic to your fetchPosts() method to setQueryData for individual posts. ie: your current fetchPosts uses the ['posts'] cache key, make it so that after the fetch call in fetchPosts, it sets query data to ['posts', '<post id here>'] by looping through the results. Then in your <Post> component, you would just pass the post ID, and inside of that component you would just do a react query targeting that cache key (you can mark it as enabled: false so it never actually triggers the API call, but it would read from the react query cache Then on upvote, your essentially now only modifying the query data for that individual cache key, instead of the global posts one
harsh-harlequin
harsh-harlequin•2mo ago
that might be very good practice actually. sounds like it's great implementation of client state - it prevents re-renders, enables not passing props too deeply, and doesn't depend on other state management libs, which you'd need to use if not using tanstack query in that case if you want to prevent re-renders
afraid-scarlet
afraid-scarlet•4w ago
I would suggest the most idiomatic way of doing this would be to have the components that render each post only use a select function so that any un-updated posts don't change. See Dominik's blog post on the topic: https://tkdodo.eu/blog/react-query-selectors-supercharged And for an example:
const allPostsQuery = queryOptions({
queryKey: ['posts'],
queryFn: fetchAllPosts
})

const useAllPosts = () => useQuery(allPostsQuery)

const useOnePost = (postId, page) =>
useQuery({
...allPostsQuery,
select: data => data.pages[page].posts.find(p => p.id === postId)
})
const allPostsQuery = queryOptions({
queryKey: ['posts'],
queryFn: fetchAllPosts
})

const useAllPosts = () => useQuery(allPostsQuery)

const useOnePost = (postId, page) =>
useQuery({
...allPostsQuery,
select: data => data.pages[page].posts.find(p => p.id === postId)
})
Now when you use just one post at a time in your components, only the affected post will return a new object and only the components subscribed to that post will rerender.
React Query Selectors, Supercharged
How to get the most out of select, sprinkled with some TypeScript tips.
vicious-gold
vicious-goldOP•4w ago
thanks, i've been implementing this solution, but found a problem with it: garbage collection in my use case, the list of posts is an infinitely-loaded, virtualized list, meaning post components aren't rendered until they're scrolled to, which means that the useQuery calls on those posts are never executed, causing those manually-set posts to get garbage-collected if they're not scrolled to within 5 minutes after setQueryData
afraid-scarlet
afraid-scarlet•4w ago
I assume that can be disabled?
vicious-gold
vicious-goldOP•4w ago
@happy not entirely: in useQuery you can set gcTime: Infinity to disable garbage collection, but that will only cover cases where you the component DOES render within 5 minutes after setQueryData, but not the cases where it doesn't a solution that seems to work for me so far is to setQueryData just before rendering the actual post component, as opposed to in the queryFn as you suggested
No description
harsh-harlequin
harsh-harlequin•4w ago
queryClient.setQueryDefaults("post", { gcTime: Infinity } - maybe that would solve the workaround of setting it "just before rendering"? since queryData allow you to only change data but withDefaults you have access to rest of the optins so you would do that before queryClient.setQueryData in parent component
harsh-harlequin
harsh-harlequin•4w ago
it actually took me a while to think how to update that default behaviour, and zookerDude also not noticing you don't need to useQuery to set the gcTime makes me think docs can be improved @TkDodo 🔮 what do you think about updating from:
If the query is not utilized by a query hook in the default gcTime of 5 minutes, the query will be garbage collected
to
If the query is not utilized by a query hook in the default gcTime of 5 minutes, the query will be garbage collected (which can be overwritten with queryClient.setQueryDefaults)
No description
ambitious-aqua
ambitious-aqua•4w ago
I'm sorry, guys, if I'm late to the party, but I think normalization can come to the rescue. The trick is to maintain an array of ids that you will map and render, and individual post queries by their id that you will pull using useQuery. Here's the pseudo-code to describe the concept:
export function useInfinitePostsIds() {
const queryClient = useQueryClient();
return useQuery({
queryKey: ['postsIds'],
queryFn: async ({ pageParam = 1 }) => {
const posts = await fetchPosts(pageParam);
const postsIds = posts.map(post => {
queryClient.setQueryData(['post', { id: post.id }], post);
return post.id;
});
return postsIds;
}
})
}

function PostsFeed() {
const { data } = useInfinitePostsIds();
const postsIds = data ? data.pages.flatMap(page => page.postsIds) : [];
...
return postsIds.map(postId => <Post id={postId} />);
}

function Post({ postId }) {
const post = useSinglePost(postId);
...
}
export function useInfinitePostsIds() {
const queryClient = useQueryClient();
return useQuery({
queryKey: ['postsIds'],
queryFn: async ({ pageParam = 1 }) => {
const posts = await fetchPosts(pageParam);
const postsIds = posts.map(post => {
queryClient.setQueryData(['post', { id: post.id }], post);
return post.id;
});
return postsIds;
}
})
}

function PostsFeed() {
const { data } = useInfinitePostsIds();
const postsIds = data ? data.pages.flatMap(page => page.postsIds) : [];
...
return postsIds.map(postId => <Post id={postId} />);
}

function Post({ postId }) {
const post = useSinglePost(postId);
...
}
Both hooks should have gcTime: Infinity.
harsh-harlequin
harsh-harlequin•4w ago
i dont think 'post' will have gcTime infinite without setting defaults like i said since useSinglePost won't be called cause Post is not rendered
vicious-gold
vicious-goldOP•2w ago
thanks @konhi, setQueryDefaults seems to be the way: i've implemented it on individual posts, and so far not getting the error from before i hope there's not a serious performance implication with this? i'm relatively inexperienced in dev, so i'm not sure
harsh-harlequin
harsh-harlequin•2w ago
you'll never know until you benchmark it! but i feel usually in web dev, if there are performance issues, then it's clearly visible

Did you find this page helpful?