TanStack Query Samples

TanStack Query (formerly React Query) examples including data fetching, caching, mutations, optimistic updates, and advanced patterns

Key Facts

Category
Data Fetching
Items
3
Format Families
sample

Sample Overview

TanStack Query (formerly React Query) examples including data fetching, caching, mutations, optimistic updates, and advanced patterns This sample set belongs to Data Fetching and can be used to test related workflows inside Elysia Tools.

💻 TanStack Query Basic Usage typescript

🟢 simple ⭐⭐

Fundamental TanStack Query patterns including queries, mutations, error handling, and loading states

⏱️ 25 min 🏷️ tanstack-query, react, data-fetching, state-management
Prerequisites: React hooks, TypeScript, Async/await, REST APIs
// TanStack Query Basic Usage Examples
// TanStack Query is a powerful data fetching and state management library for React

import { useQuery, useMutation, useQueryClient, QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState, useEffect, useCallback, useMemo } from 'react'

// 1. Setup Query Client
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // 5 minutes
      retry: 3,
      refetchOnWindowFocus: false,
    },
  },
})

// Provider component
export function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <UserProfile />
      <TodoList />
      <PostList />
    </QueryClientProvider>
  )
}

// 2. Basic Data Fetching with useQuery
interface User {
  id: number
  name: string
  email: string
  username: string
  address: {
    street: string
    city: string
    zipcode: string
  }
}

// Fetch function
const fetchUser = async (id: number): Promise<User> => {
  const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)

  if (!response.ok) {
    throw new Error('Failed to fetch user')
  }

  return response.json()
}

// User component with useQuery
function UserProfile() {
  const [userId, setUserId] = useState(1)

  const {
    data: user,
    isLoading,
    isError,
    error,
    refetch,
    isFetching,
  } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
    enabled: !!userId, // Only fetch when userId exists
    staleTime: 1000 * 60 * 10, // 10 minutes
  })

  const handleNextUser = () => {
    setUserId(prev => prev + 1)
  }

  const handlePrevUser = () => {
    setUserId(prev => Math.max(1, prev - 1))
  }

  return (
    <div className="p-6 border rounded-lg mb-6">
      <h2 className="text-2xl font-bold mb-4">User Profile</h2>

      <div className="mb-4 space-x-2">
        <button
          onClick={handlePrevUser}
          disabled={userId <= 1}
          className="px-3 py-1 bg-gray-500 text-white rounded disabled:opacity-50"
        >
          Previous
        </button>
        <span className="mx-2">User ID: {userId}</span>
        <button
          onClick={handleNextUser}
          className="px-3 py-1 bg-blue-500 text-white rounded"
        >
          Next
        </button>
        <button
          onClick={() => refetch()}
          disabled={isFetching}
          className="px-3 py-1 bg-green-500 text-white rounded disabled:opacity-50 ml-4"
        >
          {isFetching ? 'Refetching...' : 'Refetch'}
        </button>
      </div>

      {isLoading && <div className="text-blue-500">Loading user data...</div>}

      {isError && (
        <div className="text-red-500">
          Error: {error instanceof Error ? error.message : 'Unknown error'}
        </div>
      )}

      {user && (
        <div>
          <h3 className="text-xl font-semibold">{user.name}</h3>
          <p className="text-gray-600">@{user.username}</p>
          <p className="text-gray-800">{user.email}</p>
          <p className="text-gray-700">
            {user.address.street}, {user.address.city} {user.address.zipcode}
          </p>
        </div>
      )}
    </div>
  )
}

// 3. Mutations with useMutation
interface Todo {
  id: number
  title: string
  completed: boolean
  userId: number
}

const fetchTodos = async (): Promise<Todo[]> => {
  const response = await fetch('https://jsonplaceholder.typicode.com/todos')
  if (!response.ok) throw new Error('Failed to fetch todos')
  return response.json()
}

const createTodo = async (newTodo: { title: string; userId: number }): Promise<Todo> => {
  const response = await fetch('https://jsonplaceholder.typicode.com/todos', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      title: newTodo.title,
      userId: newTodo.userId,
      completed: false,
    }),
  })

  if (!response.ok) throw new Error('Failed to create todo')
  return response.json()
}

const toggleTodo = async (todoId: number, completed: boolean): Promise<Todo> => {
  const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${todoId}`, {
    method: 'PATCH',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ completed }),
  })

  if (!response.ok) throw new Error('Failed to update todo')
  return response.json()
}

function TodoList() {
  const queryClient = useQueryClient()
  const [newTodoTitle, setNewTodoTitle] = useState('')

  // Fetch todos
  const {
    data: todos = [],
    isLoading,
    isError,
    error,
  } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    staleTime: 1000 * 60 * 5, // 5 minutes
  })

  // Create todo mutation
  const createTodoMutation = useMutation({
    mutationFn: createTodo,
    onSuccess: (newTodo) => {
      // Add new todo to cache
      queryClient.setQueryData(['todos'], (oldTodos: Todo[] = []) => [
        ...oldTodos,
        newTodo,
      ])
      setNewTodoTitle('') // Clear input
    },
    onError: (error) => {
      console.error('Failed to create todo:', error)
    },
  })

  // Toggle todo mutation
  const toggleTodoMutation = useMutation({
    mutationFn: ({ id, completed }: { id: number; completed: boolean }) =>
      toggleTodo(id, completed),
    onSuccess: (updatedTodo) => {
      // Update todo in cache
      queryClient.setQueryData(['todos'], (oldTodos: Todo[] = []) =>
        oldTodos.map(todo =>
          todo.id === updatedTodo.id ? updatedTodo : todo
        )
      )
    },
  })

  const handleCreateTodo = (e: React.FormEvent) => {
    e.preventDefault()
    if (newTodoTitle.trim()) {
      createTodoMutation.mutate({
        title: newTodoTitle.trim(),
        userId: 1, // Using fixed user ID for demo
      })
    }
  }

  const handleToggleTodo = (todoId: number, completed: boolean) => {
    toggleTodoMutation.mutate({ id: todoId, completed: !completed })
  }

  if (isLoading) return <div className="p-4">Loading todos...</div>
  if (isError) return <div className="p-4 text-red-500">Error: {error instanceof Error ? error.message : 'Unknown error'}</div>

  return (
    <div className="p-6 border rounded-lg mb-6">
      <h2 className="text-2xl font-bold mb-4">Todo List</h2>

      <form onSubmit={handleCreateTodo} className="mb-4 flex gap-2">
        <input
          type="text"
          value={newTodoTitle}
          onChange={(e) => setNewTodoTitle(e.target.value)}
          placeholder="New todo title..."
          className="flex-1 px-3 py-2 border rounded"
          disabled={createTodoMutation.isPending}
        />
        <button
          type="submit"
          disabled={createTodoMutation.isPending}
          className="px-4 py-2 bg-blue-500 text-white rounded disabled:opacity-50"
        >
          {createTodoMutation.isPending ? 'Adding...' : 'Add Todo'}
        </button>
      </form>

      <ul className="space-y-2">
        {todos.slice(0, 10).map((todo) => (
          <li key={todo.id} className="flex items-center gap-2 p-2 border rounded">
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => handleToggleTodo(todo.id, todo.completed)}
              disabled={toggleTodoMutation.isPending}
              className="w-4 h-4"
            />
            <span className={todo.completed ? 'line-through text-gray-500' : ''}>
              {todo.title}
            </span>
            {toggleTodoMutation.isPending && (
              <span className="text-sm text-gray-500">Updating...</span>
            )}
          </li>
        ))}
      </ul>
    </div>
  )
}

// 4. Dependent Queries
interface Post {
  id: number
  title: string
  body: string
  userId: number
}

interface Comment {
  id: number
  postId: number
  name: string
  email: string
  body: string
}

const fetchPosts = async (userId: number): Promise<Post[]> => {
  const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}/posts`)
  if (!response.ok) throw new Error('Failed to fetch posts')
  return response.json()
}

const fetchComments = async (postId: number): Promise<Comment[]> => {
  const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}/comments`)
  if (!response.ok) throw new Error('Failed to fetch comments')
  return response.json()
}

function PostList() {
  const [selectedUserId] = useState(1)

  // First query: fetch posts for a user
  const {
    data: posts = [],
    isLoading: postsLoading,
    error: postsError,
  } = useQuery({
    queryKey: ['posts', selectedUserId],
    queryFn: () => fetchPosts(selectedUserId),
    enabled: !!selectedUserId,
  })

  // Second query: fetch comments for the first post (dependent on first query)
  const {
    data: comments = [],
    isLoading: commentsLoading,
    error: commentsError,
  } = useQuery({
    queryKey: ['comments', posts[0]?.id],
    queryFn: () => fetchComments(posts[0]?.id),
    enabled: !!posts[0]?.id, // Only run when we have a post ID
  })

  if (postsLoading) return <div className="p-4">Loading posts...</div>
  if (postsError) return <div className="p-4 text-red-500">Error loading posts</div>

  return (
    <div className="p-6 border rounded-lg">
      <h2 className="text-2xl font-bold mb-4">Posts and Comments</h2>

      <div className="mb-6">
        <h3 className="text-xl font-semibold mb-3">User Posts</h3>
        <div className="space-y-3">
          {posts.slice(0, 3).map((post) => (
            <div key={post.id} className="p-3 border rounded">
              <h4 className="font-medium">{post.title}</h4>
              <p className="text-gray-600 text-sm mt-1">{post.body.slice(0, 100)}...</p>
            </div>
          ))}
        </div>
      </div>

      {commentsLoading ? (
        <div>Loading comments...</div>
      ) : commentsError ? (
        <div className="text-red-500">Error loading comments</div>
      ) : (
        <div>
          <h3 className="text-xl font-semibold mb-3">Comments for First Post</h3>
          <div className="space-y-2">
            {comments.slice(0, 3).map((comment) => (
              <div key={comment.id} className="p-3 bg-gray-50 rounded">
                <div className="font-medium">{comment.name}</div>
                <div className="text-sm text-gray-600">{comment.email}</div>
                <div className="text-sm mt-1">{comment.body}</div>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  )
}

// 5. Pagination with Query Keys
function PaginatedComponent() {
  const [page, setPage] = useState(1)
  const pageSize = 10

  const {
    data,
    isLoading,
    isError,
    error,
  } = useQuery({
    queryKey: ['items', page, pageSize],
    queryFn: async () => {
      const response = await fetch(
        `https://jsonplaceholder.typicode.com/todos?_page=${page}&_limit=${pageSize}`
      )
      if (!response.ok) throw new Error('Failed to fetch data')
      return response.json()
    },
    keepPreviousData: true, // Keep previous data while loading new page
  })

  return (
    <div className="p-6 border rounded-lg">
      <h2 className="text-2xl font-bold mb-4">Paginated Todos</h2>

      {isLoading && <div>Loading...</div>}
      {isError && <div className="text-red-500">Error: {error instanceof Error ? error.message : 'Unknown error'}</div>}

      {data && (
        <>
          <ul className="space-y-2 mb-4">
            {data.map((item: any) => (
              <li key={item.id} className="p-2 border rounded">
                {item.title}
              </li>
            ))}
          </ul>

          <div className="flex gap-2">
            <button
              onClick={() => setPage(prev => Math.max(1, prev - 1))}
              disabled={page === 1}
              className="px-3 py-1 bg-gray-500 text-white rounded disabled:opacity-50"
            >
              Previous
            </button>
            <span className="px-3 py-1">Page {page}</span>
            <button
              onClick={() => setPage(prev => prev + 1)}
              className="px-3 py-1 bg-blue-500 text-white rounded"
            >
              Next
            </button>
          </div>
        </>
      )}
    </div>
  )
}

export { App, UserProfile, TodoList, PostList, PaginatedComponent }

💻 TanStack Query Advanced Patterns typescript

🟡 intermediate ⭐⭐⭐⭐

Advanced TanStack Query patterns including optimistic updates, infinite queries, background refetching, and custom hooks

⏱️ 45 min 🏷️ tanstack-query, advanced, optimistic, infinite, realtime
Prerequisites: TanStack Query basics, React hooks, TypeScript, Advanced patterns
// TanStack Query Advanced Patterns
// Advanced data fetching patterns and optimizations

import {
  useQuery,
  useMutation,
  useQueryClient,
  useInfiniteQuery,
  QueryClient,
  QueryClientProvider,
  useQueries,
  QueryObserver,
} from '@tanstack/react-query'
import { useState, useEffect } from 'react'

// 1. Optimistic Updates
interface Todo {
  id: number
  title: string
  completed: boolean
  userId: number
  createdAt: string
}

const fetchTodos = async (): Promise<Todo[]> => {
  const response = await fetch('https://jsonplaceholder.typicode.com/todos')
  if (!response.ok) throw new Error('Failed to fetch todos')
  return response.json()
}

const updateTodo = async (id: number, updates: Partial<Todo>): Promise<Todo> => {
  const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`, {
    method: 'PATCH',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(updates),
  })
  if (!response.ok) throw new Error('Failed to update todo')
  return response.json()
}

const deleteTodo = async (id: number): Promise<void> => {
  const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`, {
    method: 'DELETE',
  })
  if (!response.ok) throw new Error('Failed to delete todo')
}

function OptimisticTodoList() {
  const queryClient = useQueryClient()

  const {
    data: todos = [],
    isLoading,
  } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })

  const updateTodoMutation = useMutation({
    mutationFn: ({ id, updates }: { id: number; updates: Partial<Todo> }) =>
      updateTodo(id, updates),
    onMutate: async ({ id, updates }) => {
      // Cancel any outgoing refetches
      await queryClient.cancelQueries({ queryKey: ['todos'] })

      // Snapshot the previous value
      const previousTodos = queryClient.getQueryData<Todo[]>(['todos'])

      // Optimistically update to the new value
      queryClient.setQueryData<Todo[]>(['todos'], (old = []) =>
        old.map(todo =>
          todo.id === id ? { ...todo, ...updates } : todo
        )
      )

      // Return a context object with the snapshotted value
      return { previousTodos }
    },
    onError: (error, variables, context) => {
      // If the mutation fails, use the context returned from onMutate
      if (context?.previousTodos) {
        queryClient.setQueryData(['todos'], context.previousTodos)
      }
    },
    onSettled: () => {
      // Always refetch after error or success
      queryClient.invalidateQueries({ queryKey: ['todos'] })
    },
  })

  const deleteTodoMutation = useMutation({
    mutationFn: deleteTodo,
    onMutate: async (id) => {
      await queryClient.cancelQueries({ queryKey: ['todos'] })
      const previousTodos = queryClient.getQueryData<Todo[]>(['todos'])

      queryClient.setQueryData<Todo[]>(['todos'], (old = []) =>
        old.filter(todo => todo.id !== id)
      )

      return { previousTodos }
    },
    onError: (error, id, context) => {
      if (context?.previousTodos) {
        queryClient.setQueryData(['todos'], context.previousTodos)
      }
    },
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['todos'] })
    },
  })

  const handleToggleTodo = (todo: Todo) => {
    updateTodoMutation.mutate({
      id: todo.id,
      updates: { completed: !todo.completed },
    })
  }

  const handleDeleteTodo = (id: number) => {
    deleteTodoMutation.mutate(id)
  }

  if (isLoading) return <div>Loading...</div>

  return (
    <div className="p-6 border rounded-lg">
      <h2 className="text-2xl font-bold mb-4">Optimistic Todo List</h2>
      <p className="text-gray-600 mb-4">Changes appear instantly and rollback on errors</p>

      <ul className="space-y-2">
        {todos.slice(0, 10).map((todo) => (
          <li
            key={todo.id}
            className={`flex items-center gap-2 p-3 border rounded ${
              updateTodoMutation.isPending && updateTodoMutation.variables?.id === todo.id
                ? 'opacity-50'
                : ''
            }`}
          >
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => handleToggleTodo(todo)}
              disabled={updateTodoMutation.isPending}
              className="w-4 h-4"
            />
            <span className={todo.completed ? 'line-through text-gray-500' : ''}>
              {todo.title}
            </span>
            <button
              onClick={() => handleDeleteTodo(todo.id)}
              disabled={deleteTodoMutation.isPending}
              className="ml-auto px-2 py-1 text-sm bg-red-500 text-white rounded"
            >
              {deleteTodoMutation.isPending ? 'Deleting...' : 'Delete'}
            </button>
          </li>
        ))}
      </ul>
    </div>
  )
}

// 2. Infinite Scroll with useInfiniteQuery
interface Post {
  id: number
  title: string
  body: string
  userId: number
}

const fetchPostsPage = async ({ pageParam = 1 }): Promise<{
  data: Post[]
  page: number
  nextPage: number | null
}> => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/posts?_page=${pageParam}&_limit=10`
  )

  if (!response.ok) throw new Error('Failed to fetch posts')

  const posts = await response.json()
  return {
    data: posts,
    page: pageParam,
    nextPage: posts.length === 10 ? pageParam + 1 : null,
  }
}

function InfinitePostList() {
  const {
    data,
    isLoading,
    isError,
    error,
    hasNextPage,
    isFetchingNextPage,
    fetchNextPage,
  } = useInfiniteQuery({
    queryKey: ['posts-infinite'],
    queryFn: fetchPostsPage,
    getNextPageParam: (lastPage) => lastPage.nextPage,
    initialPageParam: 1,
  })

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

  return (
    <div className="p-6 border rounded-lg">
      <h2 className="text-2xl font-bold mb-4">Infinite Scroll Posts</h2>

      {isLoading && <div>Loading initial posts...</div>}
      {isError && <div className="text-red-500">Error: {error instanceof Error ? error.message : 'Unknown error'}</div>}

      <div className="space-y-4">
        {posts.map((post) => (
          <div key={post.id} className="p-4 border rounded">
            <h3 className="font-semibold text-lg">{post.title}</h3>
            <p className="text-gray-600 mt-2">{post.body}</p>
          </div>
        ))}
      </div>

      <div className="mt-6 text-center">
        <button
          onClick={() => fetchNextPage()}
          disabled={!hasNextPage || isFetchingNextPage}
          className="px-6 py-2 bg-blue-500 text-white rounded disabled:opacity-50"
        >
          {isFetchingNextPage
            ? 'Loading more...'
            : hasNextPage
            ? 'Load More'
            : 'No more posts'
          }
        </button>
      </div>
    </div>
  )
}

// 3. Multiple Queries with useQueries
function UserDashboard() {
  const [userId] = useState(1)

  const results = useQueries({
    queries: [
      {
        queryKey: ['user', userId],
        queryFn: async () => {
          const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
          if (!response.ok) throw new Error('Failed to fetch user')
          return response.json()
        },
        staleTime: 1000 * 60 * 5,
      },
      {
        queryKey: ['posts', userId],
        queryFn: async () => {
          const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}/posts`)
          if (!response.ok) throw new Error('Failed to fetch posts')
          return response.json()
        },
        staleTime: 1000 * 60 * 5,
      },
      {
        queryKey: ['todos', userId],
        queryFn: async () => {
          const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}/todos`)
          if (!response.ok) throw new Error('Failed to fetch todos')
          return response.json()
        },
        staleTime: 1000 * 60 * 5,
      },
    ],
  })

  const [userResult, postsResult, todosResult] = results
  const isLoading = results.some(result => result.isLoading)
  const isError = results.some(result => result.isError)

  if (isLoading) return <div className="p-6">Loading dashboard...</div>
  if (isError) return <div className="p-6 text-red-500">Error loading dashboard data</div>

  const user = userResult.data
  const posts = postsResult.data ?? []
  const todos = todosResult.data ?? []

  return (
    <div className="p-6 border rounded-lg">
      <h2 className="text-2xl font-bold mb-4">User Dashboard</h2>

      {user && (
        <div className="mb-6">
          <h3 className="text-xl font-semibold">{user.name}</h3>
          <p className="text-gray-600">@{user.username}</p>
          <p className="text-gray-800">{user.email}</p>
        </div>
      )}

      <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
        <div>
          <h4 className="font-semibold mb-3">Recent Posts ({posts.length})</h4>
          <div className="space-y-2">
            {posts.slice(0, 3).map((post: any) => (
              <div key={post.id} className="p-3 bg-gray-50 rounded">
                <h5 className="font-medium">{post.title}</h5>
                <p className="text-sm text-gray-600 mt-1">{post.body.slice(0, 80)}...</p>
              </div>
            ))}
          </div>
        </div>

        <div>
          <h4 className="font-semibold mb-3">Todo Status</h4>
          <div className="space-y-2">
            <div className="flex justify-between p-3 bg-green-50 rounded">
              <span>Completed</span>
              <span className="font-medium">
                {todos.filter((todo: any) => todo.completed).length}
              </span>
            </div>
            <div className="flex justify-between p-3 bg-yellow-50 rounded">
              <span>Pending</span>
              <span className="font-medium">
                {todos.filter((todo: any) => !todo.completed).length}
              </span>
            </div>
          </div>
        </div>
      </div>
    </div>
  )
}

// 4. Custom Hook Pattern
// Create a custom hook for user-related queries
function useUser(userId: number) {
  return useQuery({
    queryKey: ['user', userId],
    queryFn: async () => {
      const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
      if (!response.ok) throw new Error('Failed to fetch user')
      return response.json()
    },
    enabled: !!userId,
    staleTime: 1000 * 60 * 10, // 10 minutes
  })
}

function useUserPosts(userId: number) {
  return useQuery({
    queryKey: ['user-posts', userId],
    queryFn: async () => {
      const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}/posts`)
      if (!response.ok) throw new Error('Failed to fetch user posts')
      return response.json()
    },
    enabled: !!userId,
    staleTime: 1000 * 60 * 5, // 5 minutes
  })
}

function useCreatePost() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: async (newPost: { title: string; body: string; userId: number }) => {
      const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(newPost),
      })
      if (!response.ok) throw new Error('Failed to create post')
      return response.json()
    },
    onSuccess: (newPost, variables) => {
      // Invalidate user posts query to refetch
      queryClient.invalidateQueries({
        queryKey: ['user-posts', variables.userId]
      })
    },
  })
}

// Component using custom hooks
function UserProfileWithPosts() {
  const [userId] = useState(1)
  const [showCreateForm, setShowCreateForm] = useState(false)

  const { data: user, isLoading: userLoading, error: userError } = useUser(userId)
  const { data: posts, isLoading: postsLoading, error: postsError } = useUserPosts(userId)
  const createPostMutation = useCreatePost()

  if (userLoading || postsLoading) return <div>Loading...</div>
  if (userError || postsError) return <div>Error loading data</div>

  return (
    <div className="p-6 border rounded-lg">
      <h2 className="text-2xl font-bold mb-4">User Profile with Posts</h2>

      {user && (
        <div className="mb-6">
          <h3 className="text-xl font-semibold">{user.name}</h3>
          <p className="text-gray-600">{user.email}</p>
        </div>
      )}

      <div className="mb-4">
        <button
          onClick={() => setShowCreateForm(!showCreateForm)}
          className="px-4 py-2 bg-blue-500 text-white rounded"
        >
          {showCreateForm ? 'Hide Form' : 'Create New Post'}
        </button>
      </div>

      {showCreateForm && (
        <form
          onSubmit={(e) => {
            e.preventDefault()
            const formData = new FormData(e.currentTarget)
            createPostMutation.mutate({
              title: formData.get('title') as string,
              body: formData.get('body') as string,
              userId,
            })
          }}
          className="mb-6 p-4 border rounded bg-gray-50"
        >
          <div className="mb-3">
            <label className="block text-sm font-medium mb-1">Title</label>
            <input
              name="title"
              type="text"
              required
              className="w-full px-3 py-2 border rounded"
              disabled={createPostMutation.isPending}
            />
          </div>
          <div className="mb-3">
            <label className="block text-sm font-medium mb-1">Body</label>
            <textarea
              name="body"
              required
              rows={3}
              className="w-full px-3 py-2 border rounded"
              disabled={createPostMutation.isPending}
            />
          </div>
          <button
            type="submit"
            disabled={createPostMutation.isPending}
            className="px-4 py-2 bg-green-500 text-white rounded disabled:opacity-50"
          >
            {createPostMutation.isPending ? 'Creating...' : 'Create Post'}
          </button>
        </form>
      )}

      <div>
        <h4 className="font-semibold mb-3">Posts ({posts?.length || 0})</h4>
        <div className="space-y-3">
          {posts?.map((post: any) => (
            <div key={post.id} className="p-3 border rounded">
              <h5 className="font-medium">{post.title}</h5>
              <p className="text-gray-600 text-sm mt-1">{post.body}</p>
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}

// 5. Background Refetching and Window Focus
function BackgroundRefetchExample() {
  const [enabled, setEnabled] = useState(true)
  const [refetchInterval, setRefetchInterval] = useState(5000) // 5 seconds

  const {
    data: todos = [],
    isLoading,
    error,
  } = useQuery({
    queryKey: ['todos', 'background'],
    queryFn: fetchTodos,
    refetchInterval: enabled ? refetchInterval : false,
    refetchIntervalInBackground: true,
    refetchOnWindowFocus: enabled,
    staleTime: 1000 * 30, // 30 seconds
  })

  return (
    <div className="p-6 border rounded-lg">
      <h2 className="text-2xl font-bold mb-4">Background Refetching</h2>

      <div className="mb-4 space-y-2">
        <label className="flex items-center gap-2">
          <input
            type="checkbox"
            checked={enabled}
            onChange={(e) => setEnabled(e.target.checked)}
          />
          <span>Enable background refetching</span>
        </label>

        <div>
          <label className="block text-sm font-medium mb-1">
            Refetch Interval (ms): {refetchInterval}
          </label>
          <input
            type="range"
            min="1000"
            max="30000"
            step="1000"
            value={refetchInterval}
            onChange={(e) => setRefetchInterval(Number(e.target.value))}
            className="w-full"
          />
        </div>
      </div>

      {isLoading && <div>Loading...</div>}
      {error && <div className="text-red-500">Error loading data</div>}

      <div>
        <p className="text-sm text-gray-600 mb-2">
          Last updated: {new Date().toLocaleTimeString()}
        </p>
        <div className="text-sm text-gray-600 mb-4">
          Total todos: {todos.length}
        </div>

        <div className="space-y-2">
          {todos.slice(0, 5).map((todo: any) => (
            <div key={todo.id} className="flex items-center gap-2 p-2 border rounded">
              <input
                type="checkbox"
                checked={todo.completed}
                readOnly
                className="w-4 h-4"
              />
              <span className={todo.completed ? 'line-through text-gray-500' : ''}>
                {todo.title}
              </span>
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}

// 6. Query Observer for Manual Subscriptions
function useRealTimeData(queryKey: string[], queryFn: () => Promise<any>) {
  const [data, setData] = useState<any>(null)
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState<Error | null>(null)

  useEffect(() => {
    const queryClient = new QueryClient()

    const observer = new QueryObserver(queryClient, {
      queryKey,
      queryFn,
      refetchInterval: 5000, // Refetch every 5 seconds
    })

    const subscription = observer.subscribe((result) => {
      setLoading(result.isLoading)
      setError(result.error as Error | null)
      setData(result.data)
    })

    return () => {
      subscription.unsubscribe()
    }
  }, [queryKey.toString()])

  return { data, loading, error }
}

function RealTimeDataComponent() {
  const { data, loading, error } = useRealTimeData(
    ['realtime-todos'],
    fetchTodos
  )

  return (
    <div className="p-6 border rounded-lg">
      <h2 className="text-2xl font-bold mb-4">Real-time Data</h2>
      <p className="text-gray-600 mb-4">Updates every 5 seconds</p>

      {loading && <div>Loading...</div>}
      {error && <div className="text-red-500">Error: {error.message}</div>}

      {data && (
        <div>
          <div className="text-sm text-gray-600 mb-2">
            Total todos: {data.length}
          </div>
          <div className="text-sm text-green-600 mb-4">
            Last update: {new Date().toLocaleTimeString()}
          </div>
        </div>
      )}
    </div>
  )
}

export {
  OptimisticTodoList,
  InfinitePostList,
  UserDashboard,
  UserProfileWithPosts,
  BackgroundRefetchExample,
  RealTimeDataComponent,
}

💻 TanStack Query Data Management typescript

🔴 complex ⭐⭐⭐⭐⭐

Complete data management patterns including caching strategies, data synchronization, error boundaries, and performance optimization

⏱️ 60 min 🏷️ tanstack-query, data-management, performance, optimization
Prerequisites: Advanced TanStack Query, React patterns, TypeScript, State management
// TanStack Query Advanced Data Management
// Comprehensive data management patterns and optimizations

import {
  useQuery,
  useMutation,
  useQueryClient,
  QueryClient,
  QueryClientProvider,
  useQueries,
  QueryObserver,
  QueryCache,
  MutationCache,
} from '@tanstack/react-query'
import { useState, useEffect, useCallback, useMemo, createContext, useContext } from 'react'

// 1. Advanced Query Client Configuration
const createAdvancedQueryClient = () => {
  return new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 1000 * 60 * 5, // 5 minutes
        cacheTime: 1000 * 60 * 10, // 10 minutes
        retry: (failureCount, error) => {
          // Don't retry on 4xx errors
          if (error && typeof error === 'object' && 'status' in error) {
            const status = (error as any).status
            if (status >= 400 && status < 500) {
              return false
            }
          }
          return failureCount < 3
        },
        retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
        refetchOnWindowFocus: false,
        refetchOnReconnect: true,
        notifyOnChangeProps: ['data', 'error', 'isLoading'],
      },
      mutations: {
        retry: 1,
        onMutate: () => {
          // Show loading indicator globally
        },
        onSuccess: () => {
          // Show success notification
        },
        onError: () => {
          // Show error notification
        },
      },
    },
    queryCache: new QueryCache({
      onError: (error, query) => {
        // Global query error handler
        console.error('Query error:', error, 'Query:', query.queryKey)
      },
      onSuccess: (data, query) => {
        // Global query success handler
        console.log('Query success:', query.queryKey)
      },
    }),
    mutationCache: new MutationCache({
      onError: (error, variables, context, mutation) => {
        // Global mutation error handler
        console.error('Mutation error:', error)
      },
    }),
  })
}

// 2. Data Context for Global State Management
interface DataContextType {
  invalidateQueries: (queryKey: string[]) => void
  setQueryData: (queryKey: string[], data: any) => void
  getQueryData: (queryKey: string[]) => any
  prefetchQuery: (queryKey: string[], queryFn: () => Promise<any>) => void
}

const DataContext = createContext<DataDataContextType | null>(null)

export function DataProvider({ children }: { children: React.ReactNode }) {
  const queryClient = useQueryClient()

  const invalidateQueries = useCallback((queryKey: string[]) => {
    queryClient.invalidateQueries({ queryKey })
  }, [queryClient])

  const setQueryData = useCallback((queryKey: string[], data: any) => {
    queryClient.setQueryData(queryKey, data)
  }, [queryClient])

  const getQueryData = useCallback((queryKey: string[]) => {
    return queryClient.getQueryData(queryKey)
  }, [queryClient])

  const prefetchQuery = useCallback((queryKey: string[], queryFn: () => Promise<any>) => {
    queryClient.prefetchQuery({ queryKey, queryFn })
  }, [queryClient])

  const value = useMemo(() => ({
    invalidateQueries,
    setQueryData,
    getQueryData,
    prefetchQuery,
  }), [invalidateQueries, setQueryData, getQueryData, prefetchQuery])

  return (
    <DataContext.Provider value={value}>
      {children}
    </DataContext.Provider>
  )
}

export function useDataContext() {
  const context = useContext(DataContext)
  if (!context) {
    throw new Error('useDataContext must be used within a DataProvider')
  }
  return context
}

// 3. Smart Data Fetching Hook with Caching
interface UseSmartFetchOptions {
  enabled?: boolean
  staleTime?: number
  cacheTime?: number
  refetchOnWindowFocus?: boolean
  retry?: boolean | number
}

function useSmartFetch<T>(
  queryKey: string[],
  queryFn: () => Promise<T>,
  options: UseSmartFetchOptions = {}
) {
  const {
    enabled = true,
    staleTime = 1000 * 60 * 5,
    cacheTime = 1000 * 60 * 10,
    refetchOnWindowFocus = false,
    retry = 3,
  } = options

  return useQuery({
    queryKey,
    queryFn,
    enabled,
    staleTime,
    cacheTime,
    refetchOnWindowFocus,
    retry,
    select: useCallback((data: T) => {
      // Transform data if needed
      return data
    }, []),
  })
}

// 4. Offline-aware Data Management
function useOfflineAwareQuery<T>(
  queryKey: string[],
  queryFn: () => Promise<T>,
  options: any = {}
) {
  const [isOnline, setIsOnline] = useState(navigator.onLine)

  useEffect(() => {
    const handleOnline = () => setIsOnline(true)
    const handleOffline = () => setIsOnline(false)

    window.addEventListener('online', handleOnline)
    window.addEventListener('offline', handleOffline)

    return () => {
      window.removeEventListener('online', handleOnline)
      window.removeEventListener('offline', handleOffline)
    }
  }, [])

  return useQuery({
    queryKey,
    queryFn,
    enabled: isOnline && (options.enabled ?? true),
    retry: (failureCount, error) => {
      // Retry more aggressively when coming back online
      if (isOnline && failureCount < 5) {
        return true
      }
      return false
    },
    refetchOnWindowFocus: isOnline,
    ...options,
  })
}

// 5. Data Synchronization Patterns
class DataSyncManager {
  private queryClient: QueryClient
  private syncQueue: Array<() => Promise<void>> = []
  private isSyncing = false

  constructor(queryClient: QueryClient) {
    this.queryClient = queryClient
    this.setupOnlineListener()
  }

  private setupOnlineListener() {
    window.addEventListener('online', () => {
      this.processSyncQueue()
    })
  }

  addToSyncQueue(syncFunction: () => Promise<void>) {
    this.syncQueue.push(syncFunction)
    if (navigator.onLine && !this.isSyncing) {
      this.processSyncQueue()
    }
  }

  private async processSyncQueue() {
    if (this.isSyncing || this.syncQueue.length === 0) return

    this.isSyncing = true
    console.log('Starting sync process...')

    while (this.syncQueue.length > 0) {
      const syncFunction = this.syncQueue.shift()
      try {
        await syncFunction()
      } catch (error) {
        console.error('Sync operation failed:', error)
        // Re-queue failed operations
        if (syncFunction) {
          this.syncQueue.push(syncFunction)
        }
      }
    }

    this.isSyncing = false
    console.log('Sync process completed')
  }

  async forceSync() {
    await this.processSyncQueue()
  }
}

// 6. Cache Management Utilities
function useCacheManager() {
  const queryClient = useQueryClient()

  const clearCache = useCallback(() => {
    queryClient.clear()
  }, [queryClient])

  const clearSpecificCache = useCallback((queryKey: string[]) => {
    queryClient.removeQueries({ queryKey })
  }, [queryClient])

  const getCacheSize = useCallback(() => {
    return queryClient.getQueryCache().getAll().length
  }, [queryClient])

  const prefetchMultipleQueries = useCallback((queries: Array<{
    queryKey: string[]
    queryFn: () => Promise<any>
  }>) => {
    queries.forEach(({ queryKey, queryFn }) => {
      queryClient.prefetchQuery({ queryKey, queryFn })
    })
  }, [queryClient])

  return {
    clearCache,
    clearSpecificCache,
    getCacheSize,
    prefetchMultipleQueries,
  }
}

// 7. Advanced Error Handling with Retry Logic
function useResilientQuery<T>(
  queryKey: string[],
  queryFn: () => Promise<T>,
  options: {
    maxRetries?: number
    retryDelay?: number
    fallbackData?: T
    exponentialBackoff?: boolean
  } = {}
) {
  const {
    maxRetries = 3,
    retryDelay = 1000,
    fallbackData,
    exponentialBackoff = true,
  } = options

  return useQuery({
    queryKey,
    queryFn,
    retry: (failureCount, error: any) => {
      if (failureCount >= maxRetries) return false

      // Don't retry certain error types
      if (error?.status === 401 || error?.status === 403) {
        return false
      }

      return true
    },
    retryDelay: (attemptIndex) => {
      if (exponentialBackoff) {
        return Math.min(1000 * 2 ** attemptIndex, 30000)
      }
      return retryDelay
    },
    onError: (error) => {
      console.error('Query failed:', error)
    },
    fallbackData,
  })
}

// 8. Data Validation and Type Safety
function useValidatedQuery<T, R>(
  queryKey: string[],
  queryFn: () => Promise<T>,
  validator: (data: unknown) => R,
  options: any = {}
) {
  return useQuery({
    queryKey,
    queryFn: async () => {
      try {
        const data = await queryFn()
        return validator(data)
      } catch (error) {
        throw new Error(`Data validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`)
      }
    },
    ...options,
  })
}

// 9. Memory-efficient Data Management
function usePaginatedData<T>(
  baseQueryKey: string[],
  fetchPage: (page: number) => Promise<{ data: T[]; totalPages: number }>,
  options: {
    pageSize?: number
    cachePages?: number
  } = {}
) {
  const [currentPage, setCurrentPage] = useState(1)
  const { pageSize = 20, cachePages = 3 } = options

  const queryClient = useQueryClient()

  const {
    data,
    isLoading,
    error,
  } = useQuery({
    queryKey: [...baseQueryKey, 'page', currentPage],
    queryFn: () => fetchPage(currentPage),
    keepPreviousData: true,
    staleTime: 1000 * 60 * 5,
  })

  // Prefetch next and previous pages
  useEffect(() => {
    if (data && currentPage < data.totalPages) {
      queryClient.prefetchQuery({
        queryKey: [...baseQueryKey, 'page', currentPage + 1],
        queryFn: () => fetchPage(currentPage + 1),
      })
    }

    if (currentPage > 1) {
      queryClient.prefetchQuery({
        queryKey: [...baseQueryKey, 'page', currentPage - 1],
        queryFn: () => fetchPage(currentPage - 1),
      })
    }
  }, [currentPage, data, baseQueryKey, queryClient])

  return {
    data: data?.data ?? [],
    totalPages: data?.totalPages ?? 0,
    currentPage,
    setCurrentPage,
    isLoading,
    error,
  }
}

// 10. Real-time Data Integration
function useRealtimeSubscription<T>(
  queryKey: string[],
  initialData: T,
  subscribeToUpdates: (callback: (data: T) => void) => () => void
) {
  const queryClient = useQueryClient()

  const { data, isLoading, error } = useQuery({
    queryKey,
    queryFn: () => Promise.resolve(initialData),
    staleTime: Infinity, // Never stale, updated via subscription
  })

  useEffect(() => {
    const unsubscribe = subscribeToUpdates((newData) => {
      queryClient.setQueryData(queryKey, newData)
    })

    return unsubscribe
  }, [queryKey, queryClient, subscribeToUpdates])

  return { data, isLoading, error }
}

// 11. Comprehensive Example: E-commerce Product Management
interface Product {
  id: string
  name: string
  price: number
  description: string
  category: string
  stock: number
  images: string[]
  reviews: Review[]
}

interface Review {
  id: string
  productId: string
  userId: string
  rating: number
  comment: string
  createdAt: string
}

function ProductManagementApp() {
  const queryClient = useQueryClient()
  const [selectedCategory, setSelectedCategory] = useState<string>('all')
  const [searchTerm, setSearchTerm] = useState('')
  const [sortBy, setSortBy] = useState<'name' | 'price' | 'rating'>('name')

  // Fetch products with filters
  const {
    data: products = [],
    isLoading: productsLoading,
    error: productsError,
  } = useQuery({
    queryKey: ['products', { category: selectedCategory, searchTerm, sortBy }],
    queryFn: async () => {
      // Simulate API call
      await new Promise(resolve => setTimeout(resolve, 500))

      let filteredProducts = [
        // Mock data
        { id: '1', name: 'Laptop', price: 999, description: 'High-performance laptop', category: 'electronics', stock: 10, images: [], reviews: [] },
        { id: '2', name: 'Phone', price: 699, description: 'Smartphone', category: 'electronics', stock: 25, images: [], reviews: [] },
        { id: '3', name: 'Book', price: 19, description: 'Good book', category: 'books', stock: 100, images: [], reviews: [] },
      ]

      if (selectedCategory !== 'all') {
        filteredProducts = filteredProducts.filter(p => p.category === selectedCategory)
      }

      if (searchTerm) {
        filteredProducts = filteredProducts.filter(p =>
          p.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
          p.description.toLowerCase().includes(searchTerm.toLowerCase())
        )
      }

      return filteredProducts.sort((a, b) => {
        switch (sortBy) {
          case 'price':
            return a.price - b.price
          case 'name':
            return a.name.localeCompare(b.name)
          default:
            return 0
        }
      })
    },
    staleTime: 1000 * 60 * 5,
  })

  // Create product mutation with optimistic update
  const createProductMutation = useMutation({
    mutationFn: async (newProduct: Omit<Product, 'id' | 'reviews'>) => {
      const response = await fetch('/api/products', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(newProduct),
      })
      if (!response.ok) throw new Error('Failed to create product')
      return response.json()
    },
    onMutate: async (newProduct) => {
      await queryClient.cancelQueries({ queryKey: ['products'] })

      const previousProducts = queryClient.getQueryData<Product[]>(['products'])

      const optimisticProduct: Product = {
        ...newProduct,
        id: 'temp-' + Date.now(),
        reviews: [],
      }

      queryClient.setQueryData<Product[]>(['products'], (old = []) => [
        optimisticProduct,
        ...old,
      ])

      return { previousProducts }
    },
    onError: (error, variables, context) => {
      if (context?.previousProducts) {
        queryClient.setQueryData(['products'], context.previousProducts)
      }
    },
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['products'] })
    },
  })

  // Update product mutation
  const updateProductMutation = useMutation({
    mutationFn: async ({ id, updates }: { id: string; updates: Partial<Product> }) => {
      const response = await fetch(`/api/products/${id}`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(updates),
      })
      if (!response.ok) throw new Error('Failed to update product')
      return response.json()
    },
    onMutate: async ({ id, updates }) => {
      await queryClient.cancelQueries({ queryKey: ['products'] })

      const previousProducts = queryClient.getQueryData<Product[]>(['products'])

      queryClient.setQueryData<Product[]>(['products'], (old = []) =>
        old.map(product =>
          product.id === id ? { ...product, ...updates } : product
        )
      )

      return { previousProducts }
    },
    onError: (error, variables, context) => {
      if (context?.previousProducts) {
        queryClient.setQueryData(['products'], context.previousProducts)
      }
    },
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['products'] })
    },
  })

  return (
    <div className="p-6 border rounded-lg">
      <h2 className="text-2xl font-bold mb-4">Product Management</h2>

      {/* Filters and Search */}
      <div className="mb-6 grid grid-cols-1 md:grid-cols-4 gap-4">
        <input
          type="text"
          placeholder="Search products..."
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
          className="px-3 py-2 border rounded"
        />

        <select
          value={selectedCategory}
          onChange={(e) => setSelectedCategory(e.target.value)}
          className="px-3 py-2 border rounded"
        >
          <option value="all">All Categories</option>
          <option value="electronics">Electronics</option>
          <option value="books">Books</option>
        </select>

        <select
          value={sortBy}
          onChange={(e) => setSortBy(e.target.value as any)}
          className="px-3 py-2 border rounded"
        >
          <option value="name">Sort by Name</option>
          <option value="price">Sort by Price</option>
          <option value="rating">Sort by Rating</option>
        </select>

        <button
          onClick={() => createProductMutation.mutate({
            name: 'New Product',
            price: 99,
            description: 'New product description',
            category: 'electronics',
            stock: 50,
            images: [],
          })}
          disabled={createProductMutation.isPending}
          className="px-4 py-2 bg-green-500 text-white rounded disabled:opacity-50"
        >
          Add Product
        </button>
      </div>

      {/* Products List */}
      {productsLoading ? (
        <div>Loading products...</div>
      ) : productsError ? (
        <div className="text-red-500">Error loading products</div>
      ) : (
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
          {products.map((product) => (
            <div key={product.id} className="border rounded p-4">
              <h3 className="font-semibold">{product.name}</h3>
              <p className="text-gray-600">{product.description}</p>
              <p className="text-lg font-bold">${product.price}</p>
              <p className="text-sm text-gray-500">Stock: {product.stock}</p>
              <p className="text-sm text-gray-500">Category: {product.category}</p>

              <div className="mt-2 space-x-2">
                <button
                  onClick={() => updateProductMutation.mutate({
                    id: product.id,
                    updates: { price: product.price + 10 }
                  })}
                  disabled={updateProductMutation.isPending}
                  className="px-2 py-1 text-sm bg-blue-500 text-white rounded"
                >
                  Update Price
                </button>
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  )
}

export {
  createAdvancedQueryClient,
  DataProvider,
  useDataContext,
  useSmartFetch,
  useOfflineAwareQuery,
  DataSyncManager,
  useCacheManager,
  useResilientQuery,
  useValidatedQuery,
  usePaginatedData,
  useRealtimeSubscription,
  ProductManagementApp,
}