Irbisa · Cheatsheet

Data Fetching

Fetching in effects and its pitfalls, TanStack Query, caching and invalidation, Suspense for data, pagination, optimistic updates, errors

Middle Developer · 21 questions

  1. 01

    How do you load data from an API in a React component with useEffect, and which states do you have to handle?

    Start the request in useEffect with the request inputs in the dependency array, store the result in state, and render three states: loading, error and data.

    type User = { id: string; name: string }; function Profile({ userId }: { userId: string }) {  const [user, setUser] = useState<User | null>(null);  const [error, setError] = useState<Error | null>(null);…
  2. 02

    What is TanStack Query, and what problems does it solve compared to fetching in useEffect?

    TanStack Query (formerly React Query) is a cache for server state: you describe a request with a queryKey and a queryFn, and it handles loading and error state, caching, request deduplication, background refetching, retries and invalidation after mutations.

    import { useQuery } from '@tanstack/react-query'; type User = { id: number; name: string }; async function fetchUser(id: number): Promise<User> {  const res = await fetch(`/api/users/${id}`);…
  3. 03

    Axios or fetch: what are the practical differences, and why does fetch not reject on a 404 or 500 response?

    fetch rejects only when no response arrives (a network failure, a CORS block, an abort); a 404 or 500 is still a valid HTTP response, so the promise resolves with res.ok === false.

    // fetch: resolves for any HTTP status, rejects only when no response arrivesasync function getUser(id: string): Promise<User> {  const res = await fetch(`/api/users/${id}`);  if (!res.ok) throw new Error(`HTTP ${res.status}`); // 404 and 500 end up here  return (await res.json()) as User;}…
  4. 04

    What are the pitfalls of fetching data directly in useEffect?

    Effects run after render, on the client only, once per mounted component, so hand-written fetching gets race conditions, no cache, duplicate requests and request waterfalls.

  5. 05

    How should you design query keys in TanStack Query, and why must every variable the query uses be in the key?

    The query key is the cache address: the same key means the same cached data.

    import { queryOptions } from '@tanstack/react-query'; type Filters = { status: 'open' | 'done'; page: number }; export const todoQueries = {  all: () => ['todos'] as const,…
  6. 06

    Find the bug: clicking "Next page" changes the page number, but the list keeps showing page 1.

    function Orders() {  const [page, setPage] = useState(1);  const { data } = useQuery({    queryKey: ['orders'],    queryFn: () => fetchOrders(page),  });   return (    <>      <OrderList orders={data?.items ?? []} />      <button onClick={() => setPage((p) => p + 1)}>Next page ({page})</button>    </>  );}

    page is used in queryFn but is missing from queryKey, so the cache entry is the same for every page: TanStack Query sees an unchanged key, keeps the cached page 1 and does not refetch.

  7. 07

    What is the difference between staleTime and gcTime in TanStack Query, and what are their defaults?

    staleTime is how long fetched data counts as fresh: while fresh, it is served from cache with no refetch.

    import { QueryClient } from '@tanstack/react-query'; export const queryClient = new QueryClient({  defaultOptions: {    queries: {      staleTime: 60_000, // fresh for a minute: no refetch on mount or focus…
  8. 08

    In TanStack Query v5, what is the difference between isPending, isFetching and isLoading, and when do you use each?

    isPending means there is no data yet (status === 'pending'); isFetching means a request is running right now, including background refetches of data you already show; isLoading is both at once, the very first load.

  9. 09

    When does TanStack Query refetch data automatically, and how do you control it?

    A stale query refetches in the background when a new component using it mounts, when the window regains focus and when the network reconnects; with refetchInterval it also polls.

  10. 10

    How do you make one query wait for another (dependent queries) in TanStack Query?

    Pass the first query's result into the second one's key and turn the second off until that value exists, with enabled: !!userId or, type-safely, `queryFn: userId ?

    import { skipToken, useQuery } from '@tanstack/react-query'; function Projects({ email }: { email: string }) {  const { data: user } = useQuery({    queryKey: ['user', email],    queryFn: () => getUserByEmail(email),…
  11. 11

    How do you update data with useMutation, and how do you keep the cached lists in sync after it?

    useMutation({ mutationFn }) runs a write and tracks its isPending, error and data; it does not touch the cache on its own.

    import { useMutation, useQueryClient } from '@tanstack/react-query'; function AddTodo() {  const queryClient = useQueryClient();  const addTodo = useMutation({    mutationFn: (title: string) => api.createTodo(title),…
  12. 12

    Find the bug: after a new todo is added, the list does not show it until the page is reloaded.

    function ProjectTodos() {  const { projectId = '' } = useParams(); // "42", a string from the URL  const queryClient = useQueryClient();  const { data: todos = [] } = useQuery({    queryKey: ['projects', projectId, 'todos'],    queryFn: () => api.getTodos(projectId),    staleTime: 5 * 60_000,  });  const addTodo = useMutation({    mutationFn: api.createTodo, // resolves with the saved todo, projectId: 42    onSuccess: (todo) =>      queryClient.invalidateQueries({ queryKey: ['projects', todo.projectId, 'todos'] }),  });  return <TodoList todos={todos} onAdd={(title) => addTodo.mutate({ projectId, title })} />;}

    The mutation invalidates ['projects', 42, 'todos'] with the number from the server's response, but the list is cached under ['projects', '42', 'todos'] with the string from the URL.

  13. 13

    How do you implement an optimistic update with TanStack Query, including rollback on error?

    In onMutate, cancel in-flight queries for that key, snapshot the current cache, write the expected result with setQueryData and return the snapshot; in onError put the snapshot back; in onSettled invalidate so the cache ends up matching the server either way.

    type Todo = { id: number; title: string; done: boolean }; function useToggleTodo() {  const queryClient = useQueryClient();  return useMutation({    mutationFn: (todo: Todo) => api.updateTodo({ ...todo, done: !todo.done }),…
  14. 14

    How do you implement infinite scroll with TanStack Query?

    Use useInfiniteQuery: give it initialPageParam, a queryFn that reads pageParam, and getNextPageParam that returns the next cursor from the last page, or undefined when there is none.

    import { useInfiniteQuery } from '@tanstack/react-query'; type Page = { items: Post[]; nextCursor: string | null }; function Feed() {  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({…
  15. 15

    How do you handle errors when fetching data in React: retries, error boundaries and errors in the background?

    Decide per error where it belongs: a failed first load replaces the content (inline message or an error boundary), a failed background refetch keeps the old data with a small notice, and a failed mutation shows next to the action.

  16. 16

    How do you cancel a request that is no longer needed, for example when the user leaves the page or types a new search?

    Pass an AbortSignal to fetch and abort it when the result is no longer wanted.

    function SearchResults({ query }: { query: string }) {  const [results, setResults] = useState<Hit[]>([]);   useEffect(() => {    const controller = new AbortController();    fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal: controller.signal })…
  17. 17

    How do you refresh an expired access token when several requests fail with 401 at the same time?

    Funnel every 401 through one shared refresh promise: the first failing request starts the refresh, the others await the same promise, and then each retries once with the new token.

    let refreshing: Promise<string> | null = null; function refreshAccessToken(): Promise<string> {  // every 401 that arrives during a refresh waits for this same promise  refreshing ??= fetch('/api/auth/refresh', { method: 'POST', credentials: 'include' })    .then(async (res) => {…
  18. 18

    How does Suspense for data fetching work in React 19, and how do you use it with TanStack Query?

    A component that needs data it does not have yet suspends, and the nearest <Suspense> boundary shows its fallback until the data resolves; the component itself only ever renders the success case.

    import { Suspense } from 'react';import { useSuspenseQuery } from '@tanstack/react-query';import { ErrorBoundary } from 'react-error-boundary'; function UserName({ id }: { id: number }) {  const { data } = useSuspenseQuery({…
  19. 19

    Find the bug: this component stays on the Suspense fallback forever and the network tab shows a new request on every render.

    import { Suspense, use } from 'react'; function UserCard({ id }: { id: number }) {  const user = use(fetch(`/api/users/${id}`).then((r) => r.json()));  return <p>{user.name}</p>;} export function Page({ id }: { id: number }) {  return (    <Suspense fallback={<p>Loading...</p>}>      <UserCard id={id} />    </Suspense>  );}

    The promise is created during render, so every render passes use a brand new promise: React suspends, the request resolves, React re-renders, a new fetch starts and it suspends again.

  20. 20

    What is a request waterfall in a React app, and how do you avoid it?

    A waterfall is a chain of requests where each one starts only after the previous one finished, usually because a component fetches, then renders a child that starts its own fetch.

  21. 21

    How do you prefetch data on the server and hydrate it into TanStack Query on the client (SSR, Next.js App Router)?

    Create a QueryClient per request on the server, await queryClient.prefetchQuery(...) for what the page needs, and pass dehydrate(queryClient) to a <HydrationBoundary>; client components below it call the same useQuery and find the data already in the cache, with no loading state and no second request.

    // app/posts/page.tsx (Server Component)import { HydrationBoundary, QueryClient, dehydrate } from '@tanstack/react-query';import { postsQuery } from './queries';import { PostList } from './PostList'; // 'use client', calls useQuery(postsQuery) export default async function PostsPage() {…