Skip to content

Commit

Permalink
feat: zustand, react-query
Browse files Browse the repository at this point in the history
  • Loading branch information
warmachine028 committed Oct 24, 2024
1 parent 092e928 commit 5dd249f
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 72 deletions.
Binary file modified client/bun.lockb
Binary file not shown.
3 changes: 2 additions & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"react-intersection-observer": "^9.13.1",
"react-router-dom": "^6.27.0",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7"
"tailwindcss-animate": "^1.0.7",
"zustand": "^5.0.0"
},
"devDependencies": {
"@eslint/js": "^9.13.0",
Expand Down
16 changes: 9 additions & 7 deletions client/src/api/posts.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import axios from 'axios'
import { PostsResponse, Post } from '@/types'

const API_URL = import.meta.env.VITE_API_URL
const baseURL = import.meta.env.VITE_API_URL

const api = axios.create({ baseURL })

export const getPosts = async (skip: number = 0, limit: number = 10): Promise<PostsResponse> => {
try {
const { data } = await axios.get<PostsResponse>(`${API_URL}/posts?limit=${limit}&skip=${skip}`)
const { data } = await api.get<PostsResponse>(`/posts?limit=${limit}&skip=${skip}`)

// Add imageUrl and transform reactions to the required format for each post
const postsWithImages = data.posts.map((post) => ({
Expand All @@ -25,7 +27,7 @@ export const getPosts = async (skip: number = 0, limit: number = 10): Promise<Po
// Other API functions remain similar but need to be updated with the new Post interface
export const createPost = async (post: Omit<Post, 'id' | 'imageUrl' | 'views'>): Promise<Post> => {
try {
const { data } = await axios.post(`${API_URL}/posts/add`, post)
const { data } = await api.post(`/posts/add`, post)
return {
...data,
imageUrl: `https://picsum.photos/seed/${data.id}/800/600`,
Expand All @@ -39,7 +41,7 @@ export const createPost = async (post: Omit<Post, 'id' | 'imageUrl' | 'views'>):

export const updatePost = async (post: Partial<Post> & { id: number }): Promise<Post> => {
try {
const { data } = await axios.put(`${API_URL}/posts/${post.id}`, post)
const { data } = await api.put(`/posts/${post.id}`, post)
return {
...data,
imageUrl: `https://picsum.photos/seed/${data.id}/800/600`,
Expand All @@ -54,7 +56,7 @@ export const updatePost = async (post: Partial<Post> & { id: number }): Promise<
// Delete a post
export const deletePost = async (id: number): Promise<void> => {
try {
await axios.delete(`${API_URL}/posts/${id}`)
await api.delete(`/posts/${id}`)
} catch (error) {
throw handleApiError(error)
}
Expand All @@ -63,7 +65,7 @@ export const deletePost = async (id: number): Promise<void> => {
// Search posts
export const searchPosts = async (query: string): Promise<PostsResponse> => {
try {
const { data } = await axios.get<PostsResponse>(`${API_URL}/posts/search?q=${query}`)
const { data } = await api.get<PostsResponse>(`/posts/search?q=${query}`)

const postsWithImages = data.posts.map((post) => ({
...post,
Expand All @@ -82,7 +84,7 @@ export const searchPosts = async (query: string): Promise<PostsResponse> => {
// Get posts by user
export const getPostsByUser = async (userId: number): Promise<PostsResponse> => {
try {
const { data } = await axios.get<PostsResponse>(`${API_URL}/posts/user/${userId}`)
const { data } = await api.get<PostsResponse>(`/posts/user/${userId}`)

const postsWithImages = data.posts.map((post) => ({
...post,
Expand Down
1 change: 1 addition & 0 deletions client/src/components/AppRouter.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Link, Route, Routes, useLocation } from 'react-router-dom'
import { Posts, Todos, Vite } from '@/pages'
import { Button } from '@/components/ui'

const AppRouter = () => {
const location = useLocation()
return (
Expand Down
138 changes: 80 additions & 58 deletions client/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { PostPage } from '@/types'
import { useContext } from 'react'
import type { Post, PostPage } from '@/types'
import { useContext, useEffect } from 'react'
import { ThemeContext } from '@/contexts'
import { createPost, deletePost, getPosts, updatePost } from '@/api'
import { useInfiniteQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useStore } from '@/store'

export const useTheme = () => {
const context = useContext(ThemeContext)
Expand All @@ -14,89 +15,125 @@ export const useTheme = () => {
return context
}

export const useGetPosts = () => {
const { setOptimisticPages } = useStore()

const query = 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,
})

useEffect(() => {
if (query.data?.pages) {
setOptimisticPages(query.data.pages)
}
}, [query.data?.pages, setOptimisticPages])

return query
}

export const useCreatePost = () => {
const queryClient = useQueryClient()
const queryClient = useQueryClient() // Use QueryClient directly instead of from store
const { optimisticPages, setOptimisticPages } = useStore()

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: [] }
const optimisticPost: Post = {
...newPost,
id: Date.now(),
imageUrl: `https://picsum.photos/seed/${Date.now()}/800/600`,
views: 0,
reactions: {
likes: 0,
dislikes: 0
}
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
]
}
})
}

const updatedPages = [
{
posts: [optimisticPost],
nextCursor: optimisticPages[0]?.nextCursor
},
...optimisticPages
]

queryClient.setQueryData<{ pages: PostPage[]; pageParams: number[] }>(['posts'], (old) => ({
...(old ?? { pageParams: [] }),
pages: updatedPages
}))
setOptimisticPages(updatedPages)

return { previousData }
},
onError: (_err, _newPost, context) => {
const previousPages = (context?.previousData as { pages: PostPage[] })?.pages ?? []
queryClient.setQueryData(['posts'], context?.previousData)
setOptimisticPages(previousPages)
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] })
// Force a fresh refetch from the server
queryClient.invalidateQueries({
queryKey: ['posts'],
exact: true,
refetchType: 'all'
})
}
})
}

export const useUpdatePost = () => {
const queryClient = useQueryClient()
const { optimisticPages, setOptimisticPages } = useStore()

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
)
}))
}
})
// Update both states
const newPages = optimisticPages.map((page) => ({
...page,
posts: page.posts.map((post) => (post.id === updatedPost.id ? { ...post, ...updatedPost } : post))
}))

queryClient.setQueryData<{ pages: PostPage[]; pageParams: number[] }>(['posts'], (old) => ({
...(old ?? { pageParams: [] }),
pages: newPages
}))
setOptimisticPages(newPages)

return { previousData }
},
onError: (_err, _updatedPost, context) => {
const previousPages = (context?.previousData as { pages: PostPage[] })?.pages ?? []
queryClient.setQueryData(['posts'], context?.previousData)
setOptimisticPages(previousPages)
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] })
queryClient.invalidateQueries({
queryKey: ['posts'],
refetchType: 'all'
})
}
})
}

export const useDeletePost = () => {
const queryClient = useQueryClient()

return useMutation({
mutationFn: deletePost,
onMutate: async (postId) => {
Expand Down Expand Up @@ -125,19 +162,4 @@ export const useDeletePost = () => {
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
})
}
19 changes: 13 additions & 6 deletions client/src/pages/Posts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { useRef, useState } from 'react'
import { useInView } from 'react-intersection-observer'
import { Button, Input, Textarea, ScrollArea, Badge } from '@/components/ui'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Loader2, Plus } from 'lucide-react'
import { Loader2, Plus, RefreshCcw } from 'lucide-react'
import { motion, AnimatePresence } from 'framer-motion'
import { useCreatePost, useGetPosts } from '@/hooks'
import { Post } from '@/components'
import { useStore } from '@/store'

const CreatePost = () => {
const initialData = {
Expand Down Expand Up @@ -112,21 +113,27 @@ const CreatePost = () => {

const Posts = () => {
const { ref, inView } = useInView()
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, status } = useGetPosts()
const { optimisticPages } = useStore()
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, status, refetch, isRefetching } = useGetPosts()

if (inView && hasNextPage) {
fetchNextPage()
}

console.log(data)
return (
<div className="container mx-auto sm:p-4">
<CreatePost />

<Card className="shadow-lg h-full sm:h-auto mt-6">
<CardHeader className="bg-primary text-primary-foreground">
<CardTitle className="text-2xl font-bold">Posts</CardTitle>
<div className="flex items-center justify-between">
<CardTitle className="text-2xl font-bold">Posts</CardTitle>
<Button variant="outline" onClick={() => refetch()} className="bg-primary">
<RefreshCcw className={`${isRefetching && 'animate-spin'}`} />
</Button>
</div>
<Badge variant="secondary" className="ml-2">
{data?.pages.flatMap((page) => page.posts).length || 0} posts
{optimisticPages.flatMap((page) => page.posts).length || 0} posts
</Badge>
</CardHeader>
<CardContent className="p-6">
Expand All @@ -145,7 +152,7 @@ const Posts = () => {
exit={{ opacity: 0 }}
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 auto-rows-max"
>
{data.pages.flatMap((page) =>
{optimisticPages.map((page) =>
page.posts.map((post) => <Post key={post.id} post={post} />)
)}
</motion.div>
Expand Down
13 changes: 13 additions & 0 deletions client/src/store/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { PostPage } from '@/types'
import { create } from 'zustand'

// Updated store configuration
interface StoreState {
optimisticPages: PostPage[]
setOptimisticPages: (pages: PostPage[]) => void
}

export const useStore = create<StoreState>()((set) => ({
optimisticPages: [],
setOptimisticPages: (pages: PostPage[]) => set({ optimisticPages: pages })
}))

0 comments on commit 5dd249f

Please sign in to comment.