← Back to blog

Building a Notification System for Trading Alerts in React

Price alerts, order fills, margin warnings - designing a real-time notification architecture that informs without overwhelming.

Oliver Benns
Oliver Benns

Software engineer · Creator of Hedge UI


A trading application generates a constant stream of events that users need to know about. Orders get filled. Prices hit alert levels. Margin ratios drop below thresholds. Connections drop and reconnect. The challenge is not generating these notifications - it is presenting them without overwhelming the trader or obscuring critical information.

Notification categories

Not all notifications are equal. Categorise them by urgency:

Critical - Requires immediate attention. Liquidation warnings, connection loss, order rejections. These should be visually prominent and persistent until acknowledged.

Important - The trader should know soon. Order fills, partial fills, price alerts triggered. These appear prominently but auto-dismiss after a few seconds.

Informational - Good to know, not urgent. Connection restored, daily P&L summary, system maintenance scheduled. These appear briefly or go directly to a notification log.

type NotificationSeverity = "critical" | "important" | "info"; interface TradeNotification { id: string; severity: NotificationSeverity; title: string; message: string; timestamp: number; category: "order" | "price" | "margin" | "system"; autoDismissMs?: number; action?: { label: string; onClick: () => void; }; }

The notification store

Use a dedicated store for notifications, separate from your trading data store:

import { create } from "zustand"; interface NotificationStore { notifications: TradeNotification[]; unreadCount: number; add: (notification: Omit<TradeNotification, "id" | "timestamp">) => void; dismiss: (id: string) => void; dismissAll: () => void; markAllRead: () => void; } export const useNotificationStore = create<NotificationStore>((set) => ({ notifications: [], unreadCount: 0, add: (notification) => { const entry: TradeNotification = { ...notification, id: crypto.randomUUID(), timestamp: Date.now(), }; set((state) => ({ notifications: [entry, ...state.notifications].slice(0, 200), unreadCount: state.unreadCount + 1, })); }, dismiss: (id) => set((state) => ({ notifications: state.notifications.filter((n) => n.id !== id), })), dismissAll: () => set({ notifications: [], unreadCount: 0 }), markAllRead: () => set({ unreadCount: 0 }), }));

Cap the notification list at a reasonable size (200 here) to prevent memory growth during long trading sessions.

Toast notifications

For important and informational notifications, use toast-style popups that appear in a corner and auto-dismiss:

function NotificationToast({ notification }: { notification: TradeNotification }) { const dismiss = useNotificationStore((s) => s.dismiss); useEffect(() => { if (notification.autoDismissMs) { const timer = setTimeout(() => { dismiss(notification.id); }, notification.autoDismissMs); return () => clearTimeout(timer); } }, [notification, dismiss]); return ( <div className={cn( "pointer-events-auto w-80 rounded-lg border p-4 shadow-lg", notification.severity === "critical" && "border-red-500 bg-red-500/10", notification.severity === "important" && "border-primary bg-primary/10", notification.severity === "info" && "border-border bg-card", )}> <div className="flex items-start justify-between"> <div> <p className="text-sm font-medium">{notification.title}</p> <p className="mt-1 text-xs text-muted-foreground">{notification.message}</p> </div> <button onClick={() => dismiss(notification.id)} className="text-muted-foreground"> <X className="h-4 w-4" /> </button> </div> {notification.action && ( <button onClick={notification.action.onClick} className="mt-2 text-xs font-medium text-primary" > {notification.action.label} </button> )} </div> ); }

Position toasts in a corner that does not overlap with the order form or order book. The bottom-right works well for most trading layouts.

Critical notification handling

Critical notifications need special treatment. They should not auto-dismiss and should be visually distinct from regular toasts:

function CriticalAlert({ notification }: { notification: TradeNotification }) { return ( <div className="fixed inset-x-0 top-0 z-50 border-b border-red-500 bg-red-500/10 px-4 py-2"> <div className="flex items-center justify-between"> <div className="flex items-center gap-2"> <AlertTriangle className="h-4 w-4 text-red-500" /> <span className="text-sm font-medium text-red-500"> {notification.title}: {notification.message} </span> </div> {notification.action && ( <Button size="sm" variant="destructive" onClick={notification.action.onClick}> {notification.action.label} </Button> )} </div> </div> ); }

A banner across the top of the screen is appropriate for liquidation warnings or connection loss. These are events where the trader needs to stop what they are doing and respond.

Sound alerts

Traders often have multiple monitors and are not always looking at the trading tab. Sound alerts for important events are expected:

class SoundManager { private sounds: Map<string, HTMLAudioElement> = new Map(); private enabled: boolean = true; constructor() { this.preload("fill", "/sounds/order-fill.mp3"); this.preload("alert", "/sounds/price-alert.mp3"); this.preload("warning", "/sounds/margin-warning.mp3"); } private preload(name: string, url: string) { const audio = new Audio(url); audio.preload = "auto"; this.sounds.set(name, audio); } play(name: string) { if (!this.enabled) return; const audio = this.sounds.get(name); if (audio) { audio.currentTime = 0; audio.play().catch(() => { // Browser blocked autoplay - user needs to interact first }); } } toggle(enabled: boolean) { this.enabled = enabled; } } export const sounds = new SoundManager();

Make sound configurable per notification category. Some traders want a sound on every fill. Others only want to hear margin warnings. Store these preferences and respect them.

Generating notifications from trading events

Wire up notification generation to your trading event handlers:

function handleOrderUpdate(order: Order, previousStatus: string) { const { add } = useNotificationStore.getState(); if (order.status === "filled" && previousStatus !== "filled") { add({ severity: "important", title: "Order Filled", message: `${order.side} ${order.filledQuantity} ${order.instrument} at ${order.avgFillPrice}`, category: "order", autoDismissMs: 5000, }); sounds.play("fill"); } if (order.status === "rejected") { add({ severity: "important", title: "Order Rejected", message: `${order.instrument}: ${order.rejectReason}`, category: "order", autoDismissMs: 8000, }); } } function handleMarginUpdate(margin: MarginInfo) { const { add } = useNotificationStore.getState(); if (margin.ratio < 0.1) { add({ severity: "critical", title: "Liquidation Risk", message: `Margin ratio at ${(margin.ratio * 100).toFixed(1)}%. Reduce positions or add funds.`, category: "margin", action: { label: "View Positions", onClick: () => navigateToPositions(), }, }); sounds.play("warning"); } }

Notification log

Beyond transient toasts, provide a persistent notification log where traders can review recent events:

function NotificationLog() { const notifications = useNotificationStore((s) => s.notifications); const markAllRead = useNotificationStore((s) => s.markAllRead); useEffect(() => { markAllRead(); }, [markAllRead]); return ( <div className="flex h-full flex-col"> <div className="flex items-center justify-between border-b p-3"> <h3 className="text-sm font-medium">Notifications</h3> <span className="text-xs text-muted-foreground"> {notifications.length} events </span> </div> <div className="flex-1 overflow-y-auto"> {notifications.map((n) => ( <NotificationLogEntry key={n.id} notification={n} /> ))} </div> </div> ); }

This gives traders a way to check what happened while they were away from the screen. It is especially useful for reviewing fill history and alert triggers.

Rate limiting

During volatile markets, you might generate dozens of notifications per second as orders fill and price alerts trigger in rapid succession. Without rate limiting, the toast stack becomes a wall of overlapping popups.

Limit the visible toast count and collapse repeated notifications:

function addWithRateLimit(notification: Omit<TradeNotification, "id" | "timestamp">) { const state = useNotificationStore.getState(); const recent = state.notifications.filter( (n) => Date.now() - n.timestamp < 2000 && n.category === notification.category ); if (recent.length >= 5) { // Collapse into a summary const existing = recent[0]; useNotificationStore.getState().dismiss(existing.id); notification = { ...notification, title: `${notification.title} (+${recent.length} more)`, }; } useNotificationStore.getState().add(notification); }

The goal is to keep the trader informed without making the notification system itself a distraction. One consolidated "5 orders filled" notification is more useful than five individual toasts stacking on top of each other.

Hedge UI's demo uses sonner for the toast layer rather than rolling a notification store. A single <Toaster /> mounts at the root of the provider tree and feature modules call toast.success(...) / toast.error(...) directly from event handlers - no shared state, no global subscription. Sonner handles queueing, stacking, and dismissal, which removes a surprising amount of UI code from the application surface. The trade-off is no persistent log out of the box: teams that need fill history, sound alerts, or category filtering layer a small Zustand store on top of sonner rather than replacing it.

Kickstart Your Trading Application

Hedge UI is a React starter kit with production-ready trading components, real-time data handling, and customisable layouts — so you can ship faster.

Get Hedge UI