Bỏ qua, đến nội dung chính
Frontend14 min read28/9/2026

Hướng dẫn Cài đặt và Sử dụng TanStack Query (React Query v5) trong Next.js App Router từ A-Z

Hướng dẫn chi tiết từng bước tích hợp TanStack Query v5 vào Next.js App Router: cài đặt packages, thiết lập QueryProvider an toàn SSR, quản lý useQuery, useMutation, tự động Invalidate Cache và kỹ thuật Server Prefetching với HydrationBoundary.

N

Nguyễn Duy Noa

Frontend Developer

Trong lập trình React truyền thống, việc gọi API thường gắn liền với bộ ba: useEffect, useState(data) và useState(loading). Cách làm này dẫn đến hàng loạt vấn đề: code lặp đi lặp lại (*boilerplate*), không có cơ chế lưu bộ nhớ đệm (*caching*), dễ bị hiện tượng race-condition, và phải refetch dữ liệu thủ công mỗi khi thao tác CRUD.

TanStack Query (trước đây là React Query) là thư viện quản lý Server State (trạng thái dữ liệu bất đồng bộ) chuẩn công nghiệp hàng đầu hiện nay. Khi kết hợp với Next.js App Router, TanStack Query mang lại:

  • Tự động Caching & Deduplication: Loại bỏ triệt để các request mạng trùng lặp trên cùng một trang.
  • Background Refetching & Stale-While-Revalidate: Giao diện hiển thị ngay dữ liệu cache có sẵn và tự động cập nhật ngầm khi dữ liệu cũ.
  • Tự động Invalidate Cache: Tự động làm mới bảng dữ liệu ngay sau khi thực hiện Mutation (Create/Update/Delete).
  • Hỗ trợ Server Component Prefetching: Fetch dữ liệu trước từ server và truyền sang client mượt mà không bị màn hình trắng (*No Loading Flicker*).

Bài viết này sẽ hướng dẫn bạn từ khâu cài đặt, thiết lập Provider an toàn trên môi trường SSR cho tới các kỹ thuật nâng cao với useQuery, useMutation và HydrationBoundary.

1. Cài đặt các thư viện cần thiết

Cài đặt TanStack Query v5 và công cụ gỡ lỗi trực quan DevTools bằng package manager bạn đang dùng:

terminal
bash
# Dùng pnpm (khuyên dùng)
pnpm add @tanstack/react-query
pnpm add -D @tanstack/react-query-devtools

# Hoặc dùng npm / yarn / bun
# npm install @tanstack/react-query && npm install -D @tanstack/react-query-devtools
# yarn add @tanstack/react-query && yarn add -D @tanstack/react-query-devtools

2. Thiết lập QueryProvider chuẩn SSR trong Next.js App Router

Trong Next.js App Router, ứng dụng render trên cả Server và Client.

Cảnh báo quan trọng: Tuyệt đối không khởi tạo const queryClient = new QueryClient() ở phạm vi toàn cục (global scope) ngoài component, vì điều này sẽ làm rò rỉ dữ liệu giữa các người dùng khác nhau trên máy chủ.

Hãy tạo component QueryProvider.tsx sử dụng cơ chế Singleton an toàn:

src/components/providers/QueryProvider.tsx
typescript
"use client";

import {
  QueryClient,
  QueryClientProvider,
  isServer,
} from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { useState } from "react";

function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        // Dữ liệu được coi là mới trong 1 phút (tránh refetch liên tục)
        staleTime: 60 * 1000,
        // Tắt tự động refetch khi chuyển tab để tiết kiệm request
        refetchOnWindowFocus: false,
        // Thử lại 1 lần nếu network gặp sự cố
        retry: 1,
      },
    },
  });
}

let browserQueryClient: QueryClient | undefined = undefined;

function getQueryClient() {
  if (isServer) {
    // Server: Luôn tạo một queryClient mới cho mỗi request
    return makeQueryClient();
  } else {
    // Browser: Tạo một singleton client duy nhất nếu chưa có
    if (!browserQueryClient) browserQueryClient = makeQueryClient();
    return browserQueryClient;
  }
}

export default function QueryProvider({
  children,
}: {
  children: React.ReactNode;
}) {
  const queryClient = getQueryClient();

  return (
    <QueryClientProvider client={queryClient}>
      {children}
      {/* DevTools chỉ hiển thị ở môi trường Development */}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

3. Lấy dữ liệu mượt mà với useQuery

Tách biệt hàm gọi API thuần và Custom Hook useQuery để code rõ ràng và tái sử dụng tối đa:

src/services/blog/useBlogQueries.ts
typescript
"use client";

import { useQuery } from "@tanstack/react-query";

// 1. Interface dữ liệu
export interface Post {
  id: string;
  title: string;
  slug: string;
  views: number;
}

// 2. Hàm gọi API thuần túy
async function fetchPosts(): Promise<Post[]> {
  const res = await fetch("/api/blog/posts");
  if (!res.ok) throw new Error("Không thể tải danh sách bài viết");
  return res.json();
}

async function fetchPostDetail(slug: string): Promise<Post> {
  const res = await fetch(`/api/blog/posts/${slug}`);
  if (!res.ok) throw new Error("Không tìm thấy bài viết");
  return res.json();
}

// 3. Custom Hook useQuery
export function usePostsQuery() {
  return useQuery({
    queryKey: ["posts"],
    queryFn: fetchPosts,
    staleTime: 5 * 60 * 1000, // Dữ liệu client cache trong 5 phút
  });
}

export function usePostDetailQuery(slug: string) {
  return useQuery({
    queryKey: ["post", slug],
    queryFn: () => fetchPostDetail(slug),
    enabled: Boolean(slug), // Chỉ chạy query khi có slug
  });
}

Bạn đang gặp vấn đề tương tự?

Tôi có thể giúp bạn tối ưu website — liên hệ để trao đổi miễn phí.

4. Sử dụng trong Component: Tạm biệt useEffect & useState

Khi dùng trong giao diện, bạn chỉ cần gọi hook một dòng duy nhất:

src/features/blog/components/PostList.tsx
typescript
"use client";

import { usePostsQuery } from "@/src/services/blog/useBlogQueries";

export function PostList() {
  const { data: posts, isLoading, isError, error, refetch } = usePostsQuery();

  if (isLoading) {
    return <div className="p-4 text-zinc-400">Đang tải bài viết...</div>;
  }

  if (isError) {
    return (
      <div className="p-4 text-red-400">
        Lỗi: {error.message}
        <button onClick={() => refetch()} className="ml-2 underline">Thử lại</button>
      </div>
    );
  }

  return (
    <div className="grid gap-4">
      {posts?.map((post) => (
        <div key={post.id} className="p-4 rounded-xl bg-zinc-900 border border-zinc-800">
          <h3 className="font-semibold text-zinc-100">{post.title}</h3>
          <span className="text-xs text-zinc-500">{post.views} lượt xem</span>
        </div>
      ))}
    </div>
  );
}

5. Thao tác Dữ liệu (Create/Update/Delete) với useMutation

useMutation được sử dụng cho các thao tác làm thay đổi dữ liệu (POST, PUT, DELETE).

Điểm mạnh nhất của useMutation là khả năng tự động làm mới cache (Invalidate Queries) ngay khi request thành công, giúp bảng danh sách tự động cập nhật dữ liệu mới nhất mà không cần tải lại trang:

src/services/admin/blog/useAdminBlogMutations.ts
typescript
"use client";

import { useMutation, useQueryClient } from "@tanstack/react-query";

interface CreatePostPayload {
  title: string;
  content: string;
}

export function useCreatePostMutation() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (newPost: CreatePostPayload) => {
      const res = await fetch("/api/admin/blog", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(newPost),
      });
      if (!res.ok) throw new Error("Tạo bài viết thất bại");
      return res.json();
    },
    onSuccess: () => {
      // ⚡ Làm mới cache của danh sách bài viết
      queryClient.invalidateQueries({ queryKey: ["posts"] });
      queryClient.invalidateQueries({ queryKey: ["adminPosts"] });
    },
    onError: (err) => {
      console.error("Lỗi:", err.message);
    },
  });
}

6. Kỹ thuật Server Prefetching với HydrationBoundary

Trong Next.js App Router, để trang web đạt chuẩn SEO và người dùng không phải nhìn thấy màn hình loading khi vừa vào trang, bạn có thể Prefetch dữ liệu ngay trên Server Component (page.tsx) và truyền cache sang Client Component thông qua HydrationBoundary:

app/[locale]/blog/page.tsx
typescript
import {
  dehydrate,
  HydrationBoundary,
  QueryClient,
} from "@tanstack/react-query";
import { PostList } from "@/src/features/blog/components/PostList";

// Server Component
export default async function BlogPage() {
  const queryClient = new QueryClient();

  // 1. Fetch dữ liệu trước trên Server
  await queryClient.prefetchQuery({
    queryKey: ["posts"],
    queryFn: async () => {
      const res = await fetch("https://api.duynoa.com/api/blog/posts");
      return res.json();
    },
  });

  return (
    // 2. Hydrate dữ liệu sang Client Component
    <HydrationBoundary state={dehydrate(queryClient)}>
      <main className="container mx-auto py-12">
        <h1 className="text-3xl font-bold text-zinc-100 mb-6">Blog Công Nghệ</h1>
        {/* PostList ở client sẽ nhận data ngay lập tức mà không cần loading */}
        <PostList />
      </main>
    </HydrationBoundary>
  );
}

Kết luận

TanStack Query giải quyết triệt để bài toán quản lý trạng thái dữ liệu bất đồng bộ trong các ứng dụng web hiện đại. Khi kết hợp với kiến trúc Next.js App Router, bạn vừa có được tốc độ render tức thì từ Server Components, vừa duy trì trải nghiệm tương tác mượt mà, realtime và tự động đồng bộ cache ở phía Client.

TanStack QueryReact QueryNext.jsApp RouterReactTypeScriptData FetchingState Management

Muốn thảo luận thêm?

Nếu bạn có câu hỏi cụ thể về web performance, React, hoặc muốn hợp tác dự án — liên hệ trực tiếp với tôi.

Gửi email
Hướng dẫn Cài đặt & Sử dụng TanStack Query trong Next.js App Router | Duy Noa