React Query

Powerful data fetching and caching for Next.js

🔄 What is React Query?

React Query simplifies data fetching, caching, and synchronization in Next.js applications. It automatically manages server state, handles loading and error states, and provides powerful caching mechanisms for optimal performance.


// Simple React Query example
import { useQuery } from '@tanstack/react-query'

function Users() {
  const { data, isLoading } = useQuery({
    queryKey: ['users'],
    queryFn: () => fetch('/api/users').then(r => r.json())
  })
  
  if (isLoading) return 'Loading...'
  return <div>{data.map(user => user.name)}</div>
}
                                    

Key React Query Features

📥

Data Fetching

Fetch data with automatic caching

useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts
})
💾

Caching

Smart caching reduces API calls

useQuery({
  queryKey: ['user', id],
  staleTime: 5000
})
✏️

Mutations

Update data with useMutation

useMutation({
  mutationFn: createPost,
  onSuccess: () => {}
})
🔄

Auto Refetch

Keep data fresh automatically

useQuery({
  refetchOnWindowFocus: true,
  refetchInterval: 30000
})

🔹 Setup React Query in Next.js

React Query requires a QueryClient provider to manage cache and queries. Set it up in your root layout for App Router or _app.js for Pages Router.

// app/providers.jsx
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'

export default function Providers({ children }) {
  const [queryClient] = useState(() => new QueryClient())
  
  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  )
}

// app/layout.jsx
import Providers from './providers'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}

🔹 Basic useQuery Example

useQuery fetches data and provides loading, error, and success states automatically. The queryKey uniquely identifies the query for caching purposes.

// app/posts/page.jsx
'use client'
import { useQuery } from '@tanstack/react-query'

export default function PostsPage() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['posts'],
    queryFn: async () => {
      const res = await fetch('https://jsonplaceholder.typicode.com/posts')
      return res.json()
    }
  })

  if (isLoading) return <div>Loading posts...</div>
  if (error) return <div>Error: {error.message}</div>

  return (
    <div>
      <h1>Posts</h1>
      {data.slice(0, 5).map(post => (
        <div key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.body}</p>
        </div>
      ))}
    </div>
  )
}

Output:

Posts

Sample Post Title

This is a sample post body text that would be fetched from the API...

🔹 useMutation for Updates

useMutation handles POST, PUT, DELETE operations. It provides callbacks for success and error handling, making it easy to update UI after data changes.

// components/CreatePost.jsx
'use client'
import { useMutation, useQueryClient } from '@tanstack/react-query'

export default function CreatePost() {
  const queryClient = useQueryClient()
  
  const mutation = useMutation({
    mutationFn: async (newPost) => {
      const res = await fetch('/api/posts', {
        method: 'POST',
        body: JSON.stringify(newPost)
      })
      return res.json()
    },
    onSuccess: () => {
      // Invalidate and refetch posts
      queryClient.invalidateQueries({ queryKey: ['posts'] })
    }
  })

  const handleSubmit = (e) => {
    e.preventDefault()
    mutation.mutate({ title: 'New Post', body: 'Content' })
  }

  return (
    <form onSubmit={handleSubmit}>
      <button type="submit" disabled={mutation.isPending}>
        {mutation.isPending ? 'Creating...' : 'Create Post'}
      </button>
      {mutation.isError && <p>Error: {mutation.error.message}</p>}
    </form>
  )
}

🔹 Query with Parameters

Pass dynamic parameters to queries using the queryKey array. React Query automatically refetches when parameters change, keeping your data synchronized.

// app/user/[id]/page.jsx
'use client'
import { useQuery } from '@tanstack/react-query'

export default function UserProfile({ params }) {
  const { data: user } = useQuery({
    queryKey: ['user', params.id],
    queryFn: async () => {
      const res = await fetch(`/api/users/${params.id}`)
      return res.json()
    }
  })

  return (
    <div>
      <h1>{user?.name}</h1>
      <p>{user?.email}</p>
    </div>
  )
}

🔹 Prefetching Data

Prefetch data before users navigate to improve perceived performance. This loads data in the background so it's instantly available when needed.

// components/PostLink.jsx
'use client'
import { useQueryClient } from '@tanstack/react-query'
import Link from 'next/link'

export default function PostLink({ postId }) {
  const queryClient = useQueryClient()

  const prefetchPost = () => {
    queryClient.prefetchQuery({
      queryKey: ['post', postId],
      queryFn: () => fetch(`/api/posts/${postId}`).then(r => r.json())
    })
  }

  return (
    <Link 
      href={`/posts/${postId}`}
      onMouseEnter={prefetchPost}
    >
      View Post
    </Link>
  )
}

🧠 Test Your Knowledge

What hook is used to fetch data in React Query?