🎯 Exemples recommandés
Balanced sample collections from various categories for you to explore
Exemples TanStack Query
Exemples TanStack Query (anciennement React Query) incluant la récupération de données, la mise en cache, les mutations, les mises à jour optimistes et les patterns avancés
💻 Utilisation de Base TanStack Query typescript
🟢 simple
⭐⭐
Patterns fondamentaux TanStack Query incluant requêtes, mutations, gestion d'erreurs et états de chargement
⏱️ 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 }
💻 Patterns Avancés TanStack Query typescript
🟡 intermediate
⭐⭐⭐⭐
Patterns avancés TanStack Query incluant mises à jour optimistes, requêtes infinies, rechargement en arrière-plan et hooks personnalisés
⏱️ 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,
}
💻 Gestion de Données TanStack Query typescript
🔴 complex
⭐⭐⭐⭐⭐
Patterns complets de gestion de données incluant stratégies de cache, synchronisation de données, limites d'erreur et optimisation des performances
⏱️ 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,
}