diff --git a/client/index.html b/client/index.html
index e8d73f7..286bc6a 100644
--- a/client/index.html
+++ b/client/index.html
@@ -2,9 +2,9 @@
-
+
- Vite + React + TS
+ React Query Demo
diff --git a/client/src/api/posts.ts b/client/src/api/posts.ts
index 95469e2..96cb51c 100644
--- a/client/src/api/posts.ts
+++ b/client/src/api/posts.ts
@@ -1,25 +1,5 @@
import axios from 'axios'
-
-interface Post {
- id: number
- title: string
- body: string
- userId: number
- imageUrl: string
- tags: string[]
- reactions: {
- likes: number
- dislikes: number
- }
- views: number
-}
-
-interface PostsResponse {
- posts: Post[]
- total: number
- skip: number
- limit: number
-}
+import { PostsResponse, Post } from '@/types'
const API_URL = 'https://dummyjson.com'
diff --git a/client/src/hooks/index.ts b/client/src/hooks/index.ts
index 4bac233..0a1bc0e 100644
--- a/client/src/hooks/index.ts
+++ b/client/src/hooks/index.ts
@@ -1,5 +1,8 @@
+import { PostPage } from '@/types'
import { useContext } from 'react'
import { ThemeContext } from '@/contexts'
+import { createPost, deletePost, getPosts, updatePost } from '@/api'
+import { useInfiniteQuery, useMutation, useQueryClient } from '@tanstack/react-query'
export const useTheme = () => {
const context = useContext(ThemeContext)
@@ -10,3 +13,131 @@ export const useTheme = () => {
return context
}
+
+export const useCreatePost = () => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: createPost,
+ onMutate: async (newPost) => {
+ await queryClient.cancelQueries({ queryKey: ['posts'] })
+ const previousData = queryClient.getQueryData(['posts'])
+
+ queryClient.setQueryData<{ pages: PostPage[]; pageParams: number[] }>(['posts'], (old) => {
+ if (!old) {
+ return { pages: [], pageParams: [] }
+ }
+ return {
+ ...old,
+ pages: [
+ {
+ posts: [
+ {
+ ...newPost,
+ id: Date.now(),
+ imageUrl: `https://picsum.photos/seed/${Date.now()}/800/600`,
+ views: 0,
+ reactions: {
+ likes: 0,
+ dislikes: 0
+ }
+ }
+ ],
+ nextCursor: old.pages[0]?.nextCursor
+ },
+ ...old.pages
+ ]
+ }
+ })
+
+ return { previousData }
+ },
+ onError: (_err, _newPost, context) => {
+ queryClient.setQueryData(['posts'], context?.previousData)
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['posts'] })
+ }
+ })
+}
+
+export const useUpdatePost = () => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: updatePost,
+ onMutate: async (updatedPost) => {
+ await queryClient.cancelQueries({ queryKey: ['posts'] })
+ const previousData = queryClient.getQueryData(['posts'])
+
+ queryClient.setQueryData<{ pages: PostPage[]; pageParams: number[] }>(['posts'], (old) => {
+ if (!old) return { pages: [], pageParams: [] }
+ return {
+ ...old,
+ pages: old.pages.map((page) => ({
+ ...page,
+ posts: page.posts.map((post) =>
+ post.id === updatedPost.id ? { ...post, ...updatedPost } : post
+ )
+ }))
+ }
+ })
+
+ return { previousData }
+ },
+ onError: (_err, _updatedPost, context) => {
+ queryClient.setQueryData(['posts'], context?.previousData)
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['posts'] })
+ }
+ })
+}
+
+export const useDeletePost = () => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: deletePost,
+ onMutate: async (postId) => {
+ await queryClient.cancelQueries({ queryKey: ['posts'] })
+ const previousData = queryClient.getQueryData(['posts'])
+
+ queryClient.setQueryData<{ pages: PostPage[]; pageParams: number[] }>(['posts'], (old) => {
+ if (!old) {
+ return { pages: [], pageParams: [] }
+ }
+ return {
+ ...old,
+ pages: old.pages.map((page) => ({
+ ...page,
+ posts: page.posts.filter((post) => post.id !== postId)
+ }))
+ }
+ })
+
+ return { previousData }
+ },
+ onError: (_err, _postId, context) => {
+ queryClient.setQueryData(['posts'], context?.previousData)
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['posts'] })
+ }
+ })
+}
+
+export const useGetPosts = () => {
+ return useInfiniteQuery({
+ queryKey: ['posts'],
+ queryFn: async ({ pageParam = 0 }) => {
+ const response = await getPosts(pageParam, 10)
+ return {
+ posts: response.posts,
+ nextCursor: pageParam + 10 < response.total ? pageParam + 10 : undefined
+ }
+ },
+ getNextPageParam: (lastPage) => lastPage.nextCursor,
+ initialPageParam: 1
+ })
+}
\ No newline at end of file
diff --git a/client/src/lib/utils.ts b/client/src/lib/utils.ts
index d1d7f94..94f3f57 100644
--- a/client/src/lib/utils.ts
+++ b/client/src/lib/utils.ts
@@ -2,3 +2,5 @@ import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs))
+
+export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
diff --git a/client/src/pages/Posts.tsx b/client/src/pages/Posts.tsx
index c858cf8..f5dc8c2 100644
--- a/client/src/pages/Posts.tsx
+++ b/client/src/pages/Posts.tsx
@@ -18,25 +18,8 @@ import {
DialogTitle,
DialogTrigger
} from '@/components/ui/dialog'
-
-interface Post {
- id: number
- title: string
- body: string
- userId: number
- imageUrl: string
- tags: string[]
- reactions: {
- likes: number
- dislikes: number
- }
- views: number
-}
-
-interface PostPage {
- posts: Post[]
- nextCursor?: number
-}
+import { useCreatePost, useDeletePost, useGetPosts, useUpdatePost } from '@/hooks'
+import { type Post } from '@/types'
const tagColors = [
'bg-red-100 text-red-800',
@@ -49,134 +32,18 @@ const tagColors = [
]
export default function Posts() {
- const queryClient = useQueryClient()
const [newPostTitle, setNewPostTitle] = useState('')
const [newPostBody, setNewPostBody] = useState('')
const [newPostTags, setNewPostTags] = useState('')
- const [newPostImage, setNewPostImage] = useState(null)
+ const [_newPostImage, setNewPostImage] = useState(null)
const [editingPost, setEditingPost] = useState(null)
const [deletePostId, setDeletePostId] = useState(null)
const { ref, inView } = useInView()
- const { data, fetchNextPage, hasNextPage, isFetchingNextPage, status } = useInfiniteQuery({
- queryKey: ['posts'],
- queryFn: async ({ pageParam = 0 }) => {
- const response = await getPosts(pageParam, 10)
- return {
- posts: response.posts,
- nextCursor: pageParam + 10 < response.total ? pageParam + 10 : undefined
- }
- },
- getNextPageParam: (lastPage) => lastPage.nextCursor,
- initialPageParam: 1
- })
-
- const addMutation = useMutation({
- mutationFn: createPost,
- onMutate: async (newPost) => {
- await queryClient.cancelQueries({ queryKey: ['posts'] })
- const previousData = queryClient.getQueryData(['posts'])
-
- queryClient.setQueryData<{ pages: PostPage[]; pageParams: number[] }>(['posts'], (old) => {
- if (!old) {
- return { pages: [], pageParams: [] }
- }
- return {
- ...old,
- pages: [
- {
- posts: [
- {
- ...newPost,
- id: Date.now(),
- imageUrl: newPostImage
- ? URL.createObjectURL(newPostImage)
- : `https://picsum.photos/seed/${Date.now()}/800/600`,
- views: 0,
- reactions: {
- likes: 0,
- dislikes: 0
- }
- }
- ],
- nextCursor: old.pages[0]?.nextCursor
- },
- ...old.pages
- ]
- }
- })
-
- return { previousData }
- },
- onError: (_err, _newPost, context) => {
- queryClient.setQueryData(['posts'], context?.previousData)
- },
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['posts'] })
- setNewPostTitle('')
- setNewPostBody('')
- setNewPostTags('')
- setNewPostImage(null)
- }
- })
-
- const updateMutation = useMutation({
- mutationFn: updatePost,
- onMutate: async (updatedPost) => {
- await queryClient.cancelQueries({ queryKey: ['posts'] })
- const previousData = queryClient.getQueryData(['posts'])
-
- queryClient.setQueryData<{ pages: PostPage[]; pageParams: number[] }>(['posts'], (old) => {
- if (!old) return { pages: [], pageParams: [] }
- return {
- ...old,
- pages: old.pages.map((page) => ({
- ...page,
- posts: page.posts.map((post) =>
- post.id === updatedPost.id ? { ...post, ...updatedPost } : post
- )
- }))
- }
- })
-
- return { previousData }
- },
- onError: (_err, _updatedPost, context) => {
- queryClient.setQueryData(['posts'], context?.previousData)
- },
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['posts'] })
- setEditingPost(null)
- }
- })
-
- const deleteMutation = useMutation({
- mutationFn: deletePost,
- onMutate: async (postId) => {
- await queryClient.cancelQueries({ queryKey: ['posts'] })
- const previousData = queryClient.getQueryData(['posts'])
-
- queryClient.setQueryData<{ pages: PostPage[]; pageParams: number[] }>(['posts'], (old) => {
- if (!old) return { pages: [], pageParams: [] }
- return {
- ...old,
- pages: old.pages.map((page) => ({
- ...page,
- posts: page.posts.filter((post) => post.id !== postId)
- }))
- }
- })
-
- return { previousData }
- },
- onError: (_err, _postId, context) => {
- queryClient.setQueryData(['posts'], context?.previousData)
- },
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['posts'] })
- setDeletePostId(null)
- }
- })
+ const { data, fetchNextPage, hasNextPage, isFetchingNextPage, status } = useGetPosts()
+ const addMutation = useCreatePost()
+ const updateMutation = useUpdatePost()
+ const deleteMutation = useDeletePost()
if (inView && hasNextPage) {
fetchNextPage()
@@ -199,15 +66,15 @@ export default function Posts() {
}
})
}
+ setNewPostTitle('')
+ setNewPostBody('')
+ setNewPostTags('')
+ setNewPostImage(null)
}
- const handleUpdatePost = (_post: Post) => {
- if (editingPost) {
- updateMutation.mutate({
- ...editingPost,
- tags: editingPost.tags
- })
- }
+ const handleUpdatePost = (post: Post) => {
+ updateMutation.mutate({ ...post, tags: post.tags })
+ setEditingPost(null)
}
const handleDeletePost = (id: number) => deleteMutation.mutate(id)
diff --git a/client/src/types/index.ts b/client/src/types/index.ts
index 99ce506..a73782c 100644
--- a/client/src/types/index.ts
+++ b/client/src/types/index.ts
@@ -12,3 +12,29 @@ export type ThemeContextState = {
theme: Theme
setTheme: (theme: Theme) => void
}
+
+export interface Post {
+ id: number
+ title: string
+ body: string
+ userId: number
+ imageUrl: string
+ tags: string[]
+ reactions: {
+ likes: number
+ dislikes: number
+ }
+ views: number
+}
+
+export interface PostPage {
+ posts: Post[]
+ nextCursor?: number
+}
+
+export interface PostsResponse {
+ posts: Post[]
+ total: number
+ skip: number
+ limit: number
+}