refactor: migrate useNotifications to TanStack Query

Replace manual useState/useEffect/setInterval polling with useQuery
(refetchInterval: 60s) and useMutation for markAsRead/markAllAsRead.
Public API remains identical so consumers need no changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
CCS Admin 2026-02-26 18:30:01 +00:00
parent 5920986c02
commit 29b54e58a2

View file

@ -1,65 +1,63 @@
import { useState, useEffect, useCallback, useRef } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useCallback } from 'react'
import api from '@/services/api'
import type { Notification, NotificationList } from '@/types'
const POLL_INTERVAL = 60_000 // 60 seconds
const NOTIFICATIONS_KEY = ['notifications'] as const
async function fetchNotifications(): Promise<NotificationList> {
const res = await api.get<NotificationList>('/notifications')
return res.data
}
export function useNotifications() {
const [notifications, setNotifications] = useState<Notification[]>([])
const [unreadCount, setUnreadCount] = useState(0)
const [loading, setLoading] = useState(true)
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
const queryClient = useQueryClient()
const fetchNotifications = useCallback(async () => {
try {
const res = await api.get<NotificationList>('/notifications')
setNotifications(res.data.items)
setUnreadCount(res.data.unread_count)
} catch {
// Silently fail for polling
} finally {
setLoading(false)
}
}, [])
const { data, isLoading } = useQuery({
queryKey: NOTIFICATIONS_KEY,
queryFn: fetchNotifications,
refetchInterval: 60_000,
})
useEffect(() => {
fetchNotifications()
intervalRef.current = setInterval(fetchNotifications, POLL_INTERVAL)
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current)
}
}
}, [fetchNotifications])
const markAsRead = useCallback(async (id: number) => {
try {
const markAsReadMutation = useMutation({
mutationFn: async (id: number) => {
await api.put<Notification>(`/notifications/${id}/read`)
setNotifications((prev) =>
prev.map((n) => (n.id === id ? { ...n, is_read: true } : n))
)
setUnreadCount((prev) => Math.max(0, prev - 1))
} catch {
// ignore
}
}, [])
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: NOTIFICATIONS_KEY })
},
})
const markAllAsRead = useCallback(async () => {
try {
const markAllAsReadMutation = useMutation({
mutationFn: async () => {
await api.put<{ marked_read: number }>('/notifications/read-all')
setNotifications((prev) => prev.map((n) => ({ ...n, is_read: true })))
setUnreadCount(0)
} catch {
// ignore
}
}, [])
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: NOTIFICATIONS_KEY })
},
})
const markAsRead = useCallback(
(id: number) => {
markAsReadMutation.mutate(id)
},
[markAsReadMutation],
)
const markAllAsRead = useCallback(() => {
markAllAsReadMutation.mutate()
}, [markAllAsReadMutation])
const refresh = useCallback(() => {
queryClient.invalidateQueries({ queryKey: NOTIFICATIONS_KEY })
}, [queryClient])
return {
notifications,
unreadCount,
loading,
notifications: data?.items ?? [],
unreadCount: data?.unread_count ?? 0,
loading: isLoading,
markAsRead,
markAllAsRead,
refresh: fetchNotifications,
refresh,
}
}