All topics
Frontend · Learning hub

React-query notes for developers

Master React-query with a curated set of 1 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallMore Frontend notes
React-query

TanStack Query (React Query)

TanStack Query (React Query) Setup & Queries import { QueryClient, QueryClientProvider, useQuery, useMutation, useInfiniteQuery } from '@tanstack/react-query';

TanStack Query (React Query)

Setup & Queries

import { QueryClient, QueryClientProvider, useQuery, useMutation, useInfiniteQuery } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

// Setup — wrap app in provider
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,    // 5 min — data considered fresh
      gcTime: 10 * 60 * 1000,      // 10 min — keep inactive queries in cache
      retry: 2,
      refetchOnWindowFocus: true,
    },
  },
});

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <MyApp />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

// useQuery — fetch & cache data
function UserProfile({ userId }: { userId: string }) {
  const { data, isLoading, isError, error, refetch } = useQuery({
    queryKey: ['users', userId],       // cache key — array, be specific
    queryFn: () => api.get(`/users/${userId}`),
    enabled: !!userId,                 // only run when userId exists
    staleTime: 60_000,                 // 1 min fresh
    select: (data) => data.profile,    // transform data before return
    placeholderData: keepPreviousData, // keep old data while fetching new
  });

  if (isLoading) return <Spinner />;
  if (isError) return <Error message={error.message} />;
  return <div>{data.name}</div>;
}

// Query with dependencies
const { data: orders } = useQuery({
  queryKey: ['orders', userId, { status: 'pending' }],
  queryFn: () => api.get('/orders', { params: { userId, status: 'pending' } }),
  enabled: !!userId,
});

Mutations & Cache Updates

// useMutation
function CreatePost() {
  const queryClient = useQueryClient();

  const { mutate, mutateAsync, isPending, isError } = useMutation({
    mutationFn: (data: CreatePostInput) => api.post('/posts', data),

    // Optimistic update
    onMutate: async (newPost) => {
      await queryClient.cancelQueries({ queryKey: ['posts'] });
      const prev = queryClient.getQueryData<Post[]>(['posts']);
      queryClient.setQueryData<Post[]>(['posts'], old =>
        old ? [{ ...newPost, id: 'temp' }, ...old] : old
      );
      return { prev };                // context passed to onError
    },

    onError: (_err, _vars, context) => {
      if (context?.prev) queryClient.setQueryData(['posts'], context.prev);
    },

    onSuccess: (data) => {
      queryClient.invalidateQueries({ queryKey: ['posts'] });
      // or update cache directly:
      queryClient.setQueryData(['posts', data.id], data);
    },

    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['posts'] });
    },
  });

  return <button onClick={() => mutate({ title: 'New Post' })}>Create</button>;
}

// Prefetch
await queryClient.prefetchQuery({
  queryKey: ['users', userId],
  queryFn: () => fetchUser(userId),
});

// Invalidate & refetch
queryClient.invalidateQueries({ queryKey: ['posts'] });                // all posts
queryClient.invalidateQueries({ queryKey: ['posts', userId] });        // specific
queryClient.removeQueries({ queryKey: ['posts'] });                    // remove from cache
queryClient.resetQueries({ queryKey: ['posts'] });                     // reset to initial

Infinite Queries & Advanced

// Infinite scroll
function PostFeed() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
    isLoading,
  } = useInfiniteQuery({
    queryKey: ['posts'],
    queryFn: ({ pageParam }) =>
      api.get('/posts', { params: { cursor: pageParam, limit: 20 } }),
    initialPageParam: undefined as string | undefined,
    getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
  });

  const posts = data?.pages.flatMap(page => page.posts) ?? [];

  return (
    <div>
      {posts.map(post => <PostCard key={post.id} post={post} />)}
      <button
        onClick={() => fetchNextPage()}
        disabled={!hasNextPage || isFetchingNextPage}
      >
        {isFetchingNextPage ? 'Loading...' : 'Load more'}
      </button>
    </div>
  );
}

// useQueries — parallel queries
const results = useQueries({
  queries: userIds.map(id => ({
    queryKey: ['user', id],
    queryFn: () => fetchUser(id),
  })),
});

// Query keys factory pattern (recommended)
const postKeys = {
  all: ['posts'] as const,
  lists: () => [...postKeys.all, 'list'] as const,
  list: (filters: PostFilters) => [...postKeys.lists(), filters] as const,
  details: () => [...postKeys.all, 'detail'] as const,
  detail: (id: string) => [...postKeys.details(), id] as const,
};
// Invalidate all post queries: queryClient.invalidateQueries({ queryKey: postKeys.all })

Keep your React-query knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever