Skip to main content

Notification Hooks

The template provides a comprehensive notification system through the useAdminNotifications hook. This hook manages in-app notification state, real-time polling, read/unread status, and notification creation -- all powered by React Query for efficient cache management.

Source Location

hooks/use-admin-notifications.ts

Interfaces

Notification

The core data structure returned for each notification.

interface Notification {
id: string;
type: string; // e.g. "item_submission", "comment_reported", "user_registered"
title: string;
message: string;
data?: string; // JSON-encoded extra data (itemId, commentId, userId, etc.)
isRead: boolean;
createdAt: string;
}

NotificationStats

Computed statistics derived from the current notification set.

interface NotificationStats {
total: number;
unread: number;
byType: Record<string, number>; // count per notification type
}

CreateNotificationData

Shape of the payload when creating a new notification.

interface CreateNotificationData {
type: string;
title: string;
message: string;
data?: Record<string, any>;
}

Query Keys

The hook uses a structured query key factory for React Query cache management:

const NOTIFICATION_KEYS = {
all: ['notifications'],
lists: () => [...NOTIFICATION_KEYS.all, 'list'],
list: (filters: string) => [...NOTIFICATION_KEYS.lists(), { filters }],
details: () => [...NOTIFICATION_KEYS.all, 'detail'],
detail: (id: string) => [...NOTIFICATION_KEYS.details(), id],
stats: () => [...NOTIFICATION_KEYS.all, 'stats'],
};

This structure enables granular cache invalidation. For example, marking a notification as read invalidates the stats query while performing an optimistic update on the list query.

useAdminNotifications

Import

import { useAdminNotifications } from '@/hooks/use-admin-notifications';

Return Value

const {
// Data
notifications, // Notification[] - the current list
stats, // NotificationStats - computed totals

// Loading states
isLoading, // boolean - initial fetch in progress
isFetching, // boolean - any fetch (including background refetch)
isMarkingAsRead, // boolean - single mark-as-read in progress
isMarkingAllAsRead, // boolean - mark-all in progress
isCreating, // boolean - notification creation in progress

// Error handling
error, // string | null

// Actions
fetchNotifications, // () => void - manual refetch
markAsRead, // (notificationId: string) => Promise<result>
markAllAsRead, // () => Promise<result>
createNotification, // (data: CreateNotificationData) => Promise<result>
getNotificationLink, // (notification: Notification) => string | null
handleNotificationClick,// (notification: Notification) => Promise<void>

// Raw mutation objects (for advanced use)
markAsReadMutation,
markAllAsReadMutation,
createNotificationMutation,
} = useAdminNotifications();

Polling Configuration

The hook polls for new notifications with the following React Query settings:

SettingValuePurpose
refetchInterval5 minutesBackground polling interval
refetchIntervalInBackgroundfalseNo polling when tab is not focused
staleTime2 minutesData considered fresh for this duration
gcTime10 minutesCache retained after unmount

The query is enabled only when the user session exists (!!session?.user?.id).

Authentication Requirement

The hook reads the session from useSession() (NextAuth). All API calls are gated behind session availability -- if the user is not authenticated, the query is disabled and createNotification returns an early error.

Usage Examples

Basic Notification List

import { useAdminNotifications } from '@/hooks/use-admin-notifications';

function NotificationPanel() {
const {
notifications,
stats,
isLoading,
markAsRead,
markAllAsRead,
} = useAdminNotifications();

if (isLoading) return <Spinner />;

return (
<div>
<header>
<h2>Notifications ({stats.unread} unread)</h2>
<button onClick={() => markAllAsRead()}>
Mark All Read
</button>
</header>

{notifications.map((notif) => (
<div
key={notif.id}
className={notif.isRead ? 'opacity-60' : 'font-semibold'}
onClick={() => markAsRead(notif.id)}
>
<p>{notif.title}</p>
<span>{notif.message}</span>
</div>
))}
</div>
);
}

The handleNotificationClick method opens a relevant admin page and marks the notification as read in one call:

function NotificationItem({ notification }: { notification: Notification }) {
const { handleNotificationClick, getNotificationLink } = useAdminNotifications();

const link = getNotificationLink(notification);

return (
<div onClick={() => handleNotificationClick(notification)}>
<p>{notification.title}</p>
{link && <span className="text-xs text-blue-500">View details</span>}
</div>
);
}

The deep-link routing maps notification types to admin URLs:

Notification TypeGenerated Link
item_submission/admin/items/{itemId}
comment_reported/admin/comments/{commentId}
user_registered/admin/users/{userId}

Creating a Notification Programmatically

const { createNotification } = useAdminNotifications();

async function notifyOnSubmission(itemId: string, itemName: string) {
const result = await createNotification({
type: 'item_submission',
title: 'New Item Submitted',
message: `"${itemName}" has been submitted for review.`,
data: { itemId },
});

if (result.success) {
console.log('Notification created:', result.notification);
}
}

Notification Badge Count

function NotificationBadge() {
const { stats, isFetching } = useAdminNotifications();

if (stats.unread === 0) return null;

return (
<span className="badge">
{stats.unread}
{isFetching && <span className="animate-pulse ml-1">...</span>}
</span>
);
}

Optimistic Updates

The hook implements optimistic cache updates for both markAsRead and markAllAsRead mutations:

Single mark as read -- updates the specific notification in the cache and decrements unreadCount:

queryClient.setQueryData(NOTIFICATION_KEYS.lists(), (oldData) => ({
...oldData,
notifications: oldData.notifications.map((notif) =>
notif.id === notificationId ? { ...notif, isRead: true } : notif
),
unreadCount: Math.max(0, oldData.unreadCount - 1),
}));

Mark all as read -- sets every notification to isRead: true and resets unreadCount to 0:

queryClient.setQueryData(NOTIFICATION_KEYS.lists(), (oldData) => ({
...oldData,
notifications: oldData.notifications.map((notif) => ({
...notif,
isRead: true,
})),
unreadCount: 0,
}));

Both mutations also invalidate the stats query key to ensure consistency.

API Endpoints

The hook communicates with the following API routes:

MethodEndpointDescription
GET/api/admin/notificationsFetch all notifications with unread count
PATCH/api/admin/notifications/{id}/readMark a single notification as read
PATCH/api/admin/notifications/mark-all-readMark all notifications as read
POST/api/admin/notificationsCreate a new notification

All API calls use the serverClient utility from @/lib/api/server-api-client, which provides consistent error handling through apiUtils.isSuccess() and apiUtils.getErrorMessage().

Error Handling

Every action method (markAsRead, markAllAsRead, createNotification) is wrapped in try/catch and returns a result object:

// Success
{ success: true, notification: Notification }

// Failure
{ success: false, error: "descriptive error message" }

Mutation errors are also logged to console.error for debugging.

Stats Computation

The stats object is computed from the notification list on each render:

const stats: NotificationStats = {
total: notifications.length,
unread: unreadCount, // from API response
byType: notifications.reduce((acc, notification) => {
acc[notification.type] = (acc[notification.type] || 0) + 1;
return acc;
}, {} as Record<string, number>),
};

This provides a breakdown like { item_submission: 5, user_registered: 3 } without requiring an extra API call.