Ejemplos de TanStack Query

Ejemplos de TanStack Query (anteriormente React Query) incluyendo obtención de datos, caché, mutaciones, actualizaciones optimistas y patrones avanzados

💻 Uso Básico de TanStack Query typescript

🟢 simple ⭐⭐

Patrones fundamentales de TanStack Query incluyendo consultas, mutaciones, manejo de errores y estados de carga

⏱️ 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 } 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 }

💻 Patrones Avanzados de TanStack Query typescript

🟡 intermediate ⭐⭐⭐⭐

Patrones avanzados de TanStack Query incluyendo actualizaciones optimistas, consultas infinitas, recarga en fondo y hooks personalizados

⏱️ 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,
}

💻 Gestión de Datos de TanStack Query typescript

🔴 complex ⭐⭐⭐⭐⭐

Patrones completos de gestión de datos incluyendo estrategias de caché, sincronización de datos, límites de error y optimización de rendimiento

⏱️ 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,
}