🎯 empfohlene Sammlungen
Balanced sample collections from various categories for you to explore
TanStack Query Beispiele
TanStack Query (ehemals React Query) Beispiele einschließlich Datenabruf, Caching, Mutationen, optimistische Updates und erweiterte Patterns
💻 TanStack Query Grundlegende Nutzung typescript
🟢 simple
⭐⭐
Grundlegende TanStack Query Patterns einschließlich Queries, Mutationen, Fehlerbehandlung und Ladezustände
⏱️ 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 }
💻 TanStack Query Erweiterte Patterns typescript
🟡 intermediate
⭐⭐⭐⭐
Erweiterte TanStack Query Patterns einschließlich optimistischer Updates, unendlicher Queries, Hintergrund-Neuladen und benutzerdefinierte 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 Datenverwaltung typescript
🔴 complex
⭐⭐⭐⭐⭐
Vollständige Datenverwaltungs-Patterns einschließlich Cache-Strategien, Datensynchronisation, Fehlergrenzen und Leistungsoptimierung
⏱️ 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,
}