Skip to content

Commit 943901a

Browse files
committed
fix(notifications): avoid impure setState in markRead optimistic update
markRead called setUnreadCount from inside the setItems updater. Updater functions must be pure; React StrictMode invokes them twice in dev, double-decrementing the unread count. Compute the decrement from current items and call both setters at the top level.
1 parent 7454152 commit 943901a

1 file changed

Lines changed: 8 additions & 5 deletions

File tree

apps/web/src/components/notifications/use-notifications.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,14 @@ export function useNotifications() {
9696

9797
async function markRead(ids: string[]) {
9898
const now = new Date().toISOString();
99-
setItems((cur) => {
100-
const prevUnread = cur.filter((n) => ids.includes(n.id) && !n.readAt).length;
101-
setUnreadCount((c) => Math.max(0, c - prevUnread));
102-
return cur.map((n) => (ids.includes(n.id) ? { ...n, readAt: now } : n));
103-
});
99+
// Compute the decrement and call both setters at the top level. Calling
100+
// setUnreadCount from inside the setItems updater makes the updater impure,
101+
// so React StrictMode (which invokes updaters twice in dev) would
102+
// double-decrement the unread count.
103+
const idSet = new Set(ids);
104+
const newlyRead = items.filter((n) => idSet.has(n.id) && !n.readAt).length;
105+
setItems((cur) => cur.map((n) => (idSet.has(n.id) ? { ...n, readAt: now } : n)));
106+
setUnreadCount((c) => Math.max(0, c - newlyRead));
104107
try {
105108
await apiFetch("/api/notifications/read", { method: "POST", body: { ids } });
106109
} catch {

0 commit comments

Comments
 (0)