DEV Community

Cover image for Taking React-TanStack-Query to the Next Level in Next.js
Daniel Olawoyin
Daniel Olawoyin

Posted on

3

Taking React-TanStack-Query to the Next Level in Next.js

Remember our last chat about ditching the old fetch + useState + useEffect combo for React-TanStack-Query? If you've been using those basics—setting up the QueryProvider, writing simple queries, and handling mutations—you're probably already seeing the benefits. But here's the thing: we've only scratched the surface.

Let's dive deeper and explore some powerful techniques that'll take your data fetching game from "pretty good" to "how did you make it so fast?"

Building on Our Foundation

In our previous guide, we set up a basic movie listing with React-TanStack-Query:

const { data: movies, error, isLoading } = useQuery(['movies'], fetchMovies);
Enter fullscreen mode Exit fullscreen mode

That's great for getting started, but what if we want Netflix-like performance? You know, where everything feels instant? Let's upgrade our toolkit.

Advanced Techniques That Feel Like Magic

1. Smart Prefetching (Or: Why Netflix Feels So Fast)

Remember how our movie list worked before? Click and wait. But we can do better. Much better:

// components/MovieList.jsx
import { useQueryClient } from '@tanstack/react-query';

export default function MovieList() {
  const queryClient = useQueryClient();

  // Building on our previous fetchMovies function
  const prefetchMovie = async (movieId) => {
    await queryClient.prefetchQuery({
      queryKey: ['movie', movieId],
      queryFn: () => fetchMovieDetails(movieId),
      // Keep it fresh for 5 minutes
      staleTime: 5 * 60 * 1000,
    });
  };

  return (
    <div className="grid grid-cols-4 gap-4">
      {movies.map(movie => (
        <div 
          key={movie.id}
          onMouseEnter={() => prefetchMovie(movie.id)}
          className="movie-card"
        >
          {movie.title}
        </div>
      ))}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now when users hover over a movie, we're secretly loading the details before they click. Magic! ✨

2. Upgrading Our Mutations (Remember those?)

In our first article, we looked at basic mutations. But let's make them feel instant with optimistic updates:

// hooks/useUpdateMovie.js
export function useUpdateMovie() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: updateMovie,
    // Here's where the magic happens
    onMutate: async (newMovie) => {
      // Pause outgoing refetches
      await queryClient.cancelQueries(['movie', newMovie.id]);

      // Save current state (in case we need to roll back)
      const previousMovie = queryClient.getQueryData(['movie', newMovie.id]);

      // Update immediately (optimistically)
      queryClient.setQueryData(['movie', newMovie.id], newMovie);

      return { previousMovie };
    },
    // Uh oh, something went wrong
    onError: (err, newMovie, context) => {
      queryClient.setQueryData(
        ['movie', newMovie.id],
        context.previousMovie
      );
    },
  });
}
Enter fullscreen mode Exit fullscreen mode

3. Parallel Loading (Because Why Wait?)

Remember loading one thing at a time? Those days are over:

// pages/movie/[id].js
export default function MoviePage({ movieId }) {
  const results = useQueries({
    queries: [
      {
        queryKey: ['movie', movieId],
        queryFn: () => fetchMovie(movieId),
      },
      {
        queryKey: ['cast', movieId],
        queryFn: () => fetchCast(movieId),
      },
      {
        queryKey: ['reviews', movieId],
        queryFn: () => fetchReviews(movieId),
      },
    ],
  });

  if (results.some(result => result.isLoading)) {
    return <LoadingSpinner />;
  }

  const [movie, cast, reviews] = results.map(r => r.data);

  return <MovieDetails movie={movie} cast={cast} reviews={reviews} />;
}
Enter fullscreen mode Exit fullscreen mode

4. Infinite Scrolling (The Right Way)

Remember our paginated example? Let's upgrade it to smooth infinite scrolling:

// components/InfiniteMovieList.jsx
import { useInfiniteQuery } from '@tanstack/react-query';
import { useInView } from 'react-intersection-observer';

export default function InfiniteMovieList() {
  const { ref, inView } = useInView();

  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: ['movies'],
    queryFn: fetchMoviePage,
    getNextPageParam: (lastPage) => lastPage.nextCursor,
  });

  useEffect(() => {
    if (inView && hasNextPage) {
      fetchNextPage();
    }
  }, [inView, hasNextPage]);

  return (
    <>
      {data.pages.map((page) => (
        page.movies.map((movie) => (
          <MovieCard key={movie.id} movie={movie} />
        ))
      ))}

      <div ref={ref}>
        {isFetchingNextPage ? <LoadingSpinner /> : null}
      </div>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

5. Next.js 14 Server Component Magic

Now here's something we couldn't do in our first article—Next.js 14 server component integration:

// app/movies/page.js
import { Hydrate, dehydrate } from '@tanstack/react-query';
import { getQueryClient } from '@/utils/getQueryClient';

export default async function MoviesPage() {
  const queryClient = getQueryClient();

  // Load data during SSR
  await queryClient.prefetchQuery({
    queryKey: ['movies'],
    queryFn: fetchMovies,
  });

  const dehydratedState = dehydrate(queryClient);

  return (
    <Hydrate state={dehydratedState}>
      <MovieList />
    </Hydrate>
  );
}
Enter fullscreen mode Exit fullscreen mode

Pro Tips (Things I Wish I Knew Earlier)

  1. Consistent Query Keys: Build on our previous movie query keys:
// Instead of just ['movies']
['movies', { genre: 'action', year: 2024 }]
Enter fullscreen mode Exit fullscreen mode
  1. Smart Refetching: Remember our basic refetch on window focus? Let's make it smarter:
useQuery({
  queryKey: ['movies'],
  queryFn: fetchMovies,
  refetchOnWindowFocus: process.env.NODE_ENV === 'production',
  staleTime: 5 * 60 * 1000, // 5 minutes
});
Enter fullscreen mode Exit fullscreen mode
  1. Error Recovery: Building on our basic error handling:
<ErrorBoundary
  fallback={({ error, resetError }) => (
    <div>
      <p>Something went wrong: {error.message}</p>
      <button onClick={resetError}>Try again</button>
    </div>
  )}
>
  <MovieList />
</ErrorBoundary>
Enter fullscreen mode Exit fullscreen mode

When to Use What

  • Basic Queries (from our first article): For simple, straightforward data fetching
  • Prefetching: When you can predict the user's next move
  • Parallel Queries: When you need multiple pieces of independent data
  • Infinite Queries: For long, scrollable lists
  • Optimistic Updates: When you want that instant feel

Wrapping Up

We've come a long way from our basic setup in the first article! These advanced techniques aren't just fancy additions—they're the difference between an app that works and an app that wows.

Remember: you don't need to implement everything at once. Start with the basics we covered in the first article, then gradually add these optimizations where they make sense for your app.

Next time someone asks you "Why does your app feel so fast?", you'll know exactly why. 😎

Happy coding! And remember, there's always more to learn with React-TanStack-Query. What should we explore next?

Image of Datadog

Learn how to monitor AWS container environments at scale

In this eBook, Datadog and AWS share insights into the changing state of containers in the cloud and explore why orchestration technologies are an essential part of managing ever-changing containerized workloads.

Download the eBook

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more