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>
);
};