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