12

Imagine a typical use-case when you have a list of items and an item view.

So there is an endpoint to get all items. But also you can fetch a single item with /items/:id.

But you can avoid fetching a single item if it's already fetched from the /items endpoint. So how would you handle that with react-query?

function Items() {
  cosnt itemsQuery = useQuery('items', fetchItems);
  // render items
}
      
function SingleItem({ id }) {
  // you can have another query here and refetch item from server
  // but how to reuse an item from query cache and fetch only if there is no such item?
}

1 Answer 1

21

Docs: https://tanstack.com/query/latest/docs/framework/react/guides/initial-query-data

Advanced explanation (examples are partially taken from there): https://tkdodo.eu/blog/seeding-the-query-cache#seeding-details-from-lists

  1. You can pull the details from the list
const useTodo = (id: number) => {
  const queryClient = useQueryClient()
  return useQuery({
    queryKey: ['todos', 'detail', id],
    queryFn: () => fetchTodo(id),
    initialData: () => {
      return queryClient
        .getQueryData(['todos', 'list'])
        ?.find((todo) => todo.id === id)
    },
    
    // It will tell React Query when the data we are passing in as initialData 
    // was originally fetched, so it can determine staleness correctly
    initialDataUpdatedAt: () =>     
      // ⬇️ get the last fetch time of the list
      queryClient.getQueryState(['todos', 'list'])?.dataUpdatedAt,
  })
}
  1. Alternatively you can create initialData for all your per-item queries when you fetch the list like this:
const useTodosList = () => {
  const queryClient = useQueryClient()
  return useQuery({
    queryKey: ['todos', 'list'],
    queryFn: async () => {
      const todos = await fetchTodos()
      todos.forEach((todo) => {
        // ⬇️ create a detail cache for each item
        queryClient.setQueryData(['todos', 'detail', todo.id], todo)
      })
      return todos
    },
  })
}

const useTodo = (id: number) => {
  const queryClient = useQueryClient()
  return useQuery({
    queryKey: ['todos', 'detail', id],
    queryFn: () => fetchTodo(id),
    // ➡️ no need to set initial data manually as it is pushed by the list
  })
}

Keep in mind that both approaches only work well if the structure of your detail query is exactly the same (or at least assignable to) the structure of the list query. If the detail view has a mandatory field that doesn't exist in the list, seeding via initialData is not a good idea. This is where placeholderData comes in, and I've written a comparison about the two in #9: Placeholder and Initial Data in React Query.

From https://tkdodo.eu/blog/seeding-the-query-cache#seeding-details-from-lists

Sign up to request clarification or add additional context in comments.

4 Comments

What if it is not loaded yet? How do you trigger it to fetch?
@chrisjlee you can use ensureQueryData instead of getQueryData. This will fetch the data if it does not exist yet.
How is this efficient? As your queries grow, linear search is inefficient. See the "push approach" described here: tkdodo.eu/blog/…
@adi518 thx fo the link, good point

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.