Live notifications are a key feature of the blog application: when someone likes your post or leaves a comment, you should see a bell icon light up immediately — not on the next page load. The architecture is straightforward: FastAPI’s ConnectionManager tracks each user’s active WebSocket connections; the HTTP endpoints that create likes and comments call manager.send_to_user() after saving to the database; React listens on the WebSocket and updates the notification badge in real time.
useNotifications Hook
// src/hooks/useNotifications.js
import { useState, useCallback } from "react";
import { useWebSocket } from "@/hooks/useWebSocket";
import { useAuthStore } from "@/stores/authStore";
export function useNotifications() {
const isLoggedIn = useAuthStore((s) => Boolean(s.user));
const [notifications, setNotifications] = useState([]);
const [unreadCount, setUnreadCount] = useState(0);
const handleMessage = useCallback((data) => {
switch (data.type) {
case "notification":
setNotifications((prev) => [data, ...prev].slice(0, 20));
setUnreadCount((c) => c + 1);
break;
case "notification_read":
setUnreadCount(0);
break;
case "pong":
// heartbeat response — ignore
break;
}
}, []);
const { status, send } = useWebSocket("/ws/notifications", {
onMessage: handleMessage,
enabled: isLoggedIn,
});
function markAllRead() {
setUnreadCount(0);
send({ type: "mark_read" });
}
return { notifications, unreadCount, markAllRead, connectionStatus: status };
}
GET /api/notifications on mount to show existing unread notifications from before the current session, then layer real-time WebSocket updates on top. The initial HTTP fetch ensures the user sees notifications that arrived while they were offline; the WebSocket keeps the count current while they are online.setNotifications(prev => [data, ...prev].slice(0, 20)). A user who leaves a tab open for hours should not have thousands of notification objects in memory. For the full notification history, rely on the HTTP endpoint and database pagination — the in-memory array is just a live preview of recent activity.manager.send_to_user(recipient_id, data). A bug that broadcasts notifications to all connected users would leak private information (new comment content, like details) to every logged-in user simultaneously.Notification Bell Component
import { useState, useRef, useEffect } from "react";
import { useNotifications } from "@/hooks/useNotifications";
function NotificationBell() {
const { notifications, unreadCount, markAllRead } = useNotifications();
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
// Close dropdown when clicking outside
useEffect(() => {
function handleClickOutside(e) {
if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
setIsOpen(false);
}
}
if (isOpen) document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [isOpen]);
function handleToggle() {
setIsOpen((prev) => !prev);
if (!isOpen && unreadCount > 0) markAllRead();
}
return (
<div className="relative" ref={dropdownRef}>
{/* Bell button with badge */}
<button onClick={handleToggle}
className="relative p-2 rounded-full hover:bg-gray-100"
aria-label={`Notifications${unreadCount > 0 ? ` (${unreadCount} unread)` : ""}`}>
🔔
{unreadCount > 0 && (
<span className="absolute -top-0.5 -right-0.5 bg-red-500 text-white
text-xs w-5 h-5 rounded-full flex items-center justify-center
font-bold">
{unreadCount > 9 ? "9+" : unreadCount}
</span>
)}
</button>
{/* Dropdown */}
{isOpen && (
<div className="absolute right-0 mt-2 w-80 bg-white rounded-xl shadow-lg
border border-gray-100 z-50">
<div className="px-4 py-3 border-b border-gray-100 font-semibold text-sm">
Notifications
</div>
<ul className="max-h-80 overflow-y-auto divide-y divide-gray-50">
{notifications.length === 0 ? (
<li className="px-4 py-6 text-center text-sm text-gray-400">
No new notifications
</li>
) : (
notifications.map((n, i) => (
<li key={i} className="px-4 py-3 text-sm hover:bg-gray-50">
<p className="font-medium">{n.message ?? n.event}</p>
{n.post_title && (
<p className="text-gray-500 text-xs mt-0.5 truncate">
{n.post_title}
</p>
)}
</li>
))
)}
</ul>
</div>
)}
</div>
);
}
FastAPI Side — Pushing Notifications from HTTP Endpoints
# In app/routers/comments.py
@router.post("/posts/{post_id}/comments", response_model=CommentResponse, status_code=201)
async def create_comment(
post_id: int,
data: CommentCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
comment = create_comment_service(db, post_id, current_user.id, data)
post = db.get(Post, post_id)
# Push real-time notification to post author (if not self-commenting)
if post and post.author_id != current_user.id:
await manager.send_to_user(post.author_id, {
"type": "notification",
"event": "new_comment",
"message": f"{current_user.name} commented on your post",
"post_id": post_id,
"post_title": post.title,
})
return comment
Common Mistakes
Mistake 1 — Not fetching existing notifications on mount
❌ Wrong — user sees empty notification list on page load even if they have unread notifications from before the session.
✅ Correct — fetch GET /api/notifications?unread=true on mount to initialise the list.
Mistake 2 — Broadcasting notifications to all users
❌ Wrong — sending to all WebSocket connections instead of just the post author.
✅ Correct — always use manager.send_to_user(recipient_id, data), never manager.broadcast(data) for private notifications.