import {
useQuery,
useMutation,
useQueryClient,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query';
import axios from 'axios';
// ============================================
// Setup
// ============================================
// Create a client
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 30, // 30 minutes
retry: 1,
refetchOnWindowFocus: false,
},
},
});
// API Client
const api = axios.create({
baseURL: '/api',
});
// ============================================
// Types
// ============================================
interface Post {
id: number;
title: string;
body: string;
}
interface CreatePostDto {
title: string;
body: string;
}
// ============================================
// Hooks
// ============================================
// Fetch posts
export const usePosts = (limit = 10) => {
return useQuery({
queryKey: ['posts', limit],
queryFn: async () => {
const { data } = await api.get<Post[]>(`/posts?limit=${limit}`);
return data;
},
placeholderData: (previousData) => previousData, // Keep previous data while fetching
});
};
// Fetch single post
export const usePost = (id: number) => {
return useQuery({
queryKey: ['post', id],
queryFn: async () => {
const { data } = await api.get<Post>(`/posts/${id}`);
return data;
},
enabled: !!id, // Only fetch if id is present
});
};
// Create post mutation
export const useCreatePost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (newPost: CreatePostDto) => {
const { data } = await api.post<Post>('/posts', newPost);
return data;
},
onMutate: async (newPost) => {
// Optimistic Update
await queryClient.cancelQueries({ queryKey: ['posts'] });
const previousPosts = queryClient.getQueryData<Post[]>(['posts']);
queryClient.setQueryData<Post[]>(['posts'], (old) => [
{ id: Date.now(), ...newPost }, // Temporary optimistic post
...(old || []),
]);
return { previousPosts };
},
onError: (err, newPost, context) => {
// Rollback on error
if (context?.previousPosts) {
queryClient.setQueryData(['posts'], context.previousPosts);
}
},
onSettled: () => {
// Refetch to ensure data is correct
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
};
// ============================================
// React Components
// ============================================
export const PostsList = () => {
const { data: posts, isLoading, isError } = usePosts();
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error loading posts </div>;
return (
<ul>
{ posts?.map((post) => (
<li key= { post.id } > { post.title } </li>
))}
</ul>
);
};
export const CreatePostForm = () => {
const mutation = useCreatePost();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
mutation.mutate({ title: 'New Post', body: 'Content...' });
};
return (
<form onSubmit= { handleSubmit } >
<button disabled={ mutation.isPending }>
{ mutation.isPending ? 'Creating...' : 'Create Post' }
</button>
{ mutation.isError && <div>Error: { mutation.error.message } </div> }
</form>
);
};