Skip to content

NotificationsTab.updateNotification builds its save payload from a stale notifications closure #892

Description

@Jagadeeshftw

Description

src/features/settings/components/notifications/NotificationsTab.tsx's updateNotification saves the entire notification-settings object on every single toggle (the API is a full-object PUT, not a per-key patch):

const updateNotification = async (key: keyof NotificationSettings, value: boolean) => {
  const previousValue = notifications[key]

  // Optimistic Update
  setNotifications((prev) => ({ ...prev, [key]: value }))
  setUpdatingKeys((prev) => ({ ...prev, [key]: true }))

  try {
    const nextSettings = { ...notifications, [key]: value }
    await updateNotificationSettings(nextSettings)
  } catch (err) {
    ...

The optimistic UI update correctly uses the functional form of setNotifications (prev => ({...})), but nextSettings — the object actually sent to the backend — is built from the notifications variable captured in this render's closure, not from the latest state. If a second toggle is triggered (e.g. a fast double-click on two adjacent switches, or a "select all"-style batch of calls) before this render's notifications closure is updated by React, both calls read the same pre-update notifications snapshot and each PUTs a full settings object missing the other call's change. Whichever request's response lands second silently overwrites the other toggle's change on the backend — the UI can show both switches "on" while the server only persisted one of them, with no error surfaced (the request itself succeeds; it just carries stale data).

enableAll in the same file has an analogous pattern, computing allEnabled from notifications read at call time rather than via a functional updater.

Requirements

  • updateNotification must build the payload it sends to updateNotificationSettings from the latest settings state, not a closure captured at handler-invocation time (e.g. by using a functional state update to read the current value, or a ref that always tracks the latest notifications).
  • Apply the same fix to enableAll if it has the same closure-staleness issue.

Suggested execution

  1. Fork the repo and create a branch: git checkout -b fix/notificationstab-stale-closure
  2. Change updateNotification to compute nextSettings from the functional updater's result (e.g. capture it via a ref updated inside setNotifications(prev => {...; latestRef.current = next; return next}), or restructure to await the state update before building the payload).
  3. Apply the same pattern to enableAll.
  4. Add a test that rapidly triggers two different updateNotification calls in the same tick and asserts the final updateNotificationSettings call includes both changes, not just the last one.

Example commit message

fix: build NotificationsTab save payload from latest state, not a stale closure

Acceptance criteria

  • Two notification toggles fired in quick succession both persist to the backend; neither silently overwrites the other.
  • enableAll is verified to not have the same stale-closure issue (or is fixed alongside).
  • A test covers the rapid-consecutive-toggle race.

Security notes

None; this is a data-integrity/correctness issue — a dropped notification-preference change is low severity, but the underlying pattern (full-object PUT built from a stale closure) is worth flagging since it would be more serious on a payload with higher-stakes fields.

Guidelines

  • Minimum 95% test coverage
  • Timeframe: 96 hours

Metadata

Metadata

Assignees

Labels

GrantFox OSSGrantFox open-source programMaybe RewardedGrantFox: potentially rewarded contributionOfficial Campaign | FWC26GrantFox official campaign issuebugSomething isn't workingfrontendFrontend / UI work

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions