Skip to content

🐛 Bug Report: notifications.cache.unshift does not dedupe by id, causing duplicate notifications from useNotifications() / cache.getAll() #10762

@Alexander6060

Description

@Alexander6060

📜 Description

When using @novu/js with useCache: true, the in-memory notifications cache does not dedupe entries by notification.id on insert. If the same notification payload is written into the cache more than once: for example, when multiple Novu client instances (multiple browser tabs, multiple React roots, etc.) receive the same new notification socket event - the cache returns the same notification multiple times from getAll(), and @novu/react's useNotifications() hook consequently hands back duplicate entries to consumers.

The duplicate entries disappear on a full refresh (which replays from the server-side /notifications HTTP response) but reappear the next time a notification arrives while multiple subscribers are active. The count of duplicates scales linearly with the number of mounted Novu clients (3 tabs -> 3 copies of each newly received notification).

👟 Reproduction steps

Minimum reproducible example with @novu/js directly:

const { Novu } = require('@novu/js');

  const novu = new Novu({
    applicationIdentifier: 'app',
    subscriber: 'sub',
    useCache: true,
  });

  const args = { archived: false, limit: 10 };
  const notification = {
    id: 'n1',
    transactionId: 't1',
    createdAt: new Date().toISOString(),
    isRead: false,
    isSeen: false,
    isArchived: false,
    isSnoozed: false,
    channelType: 'in_app',
    to: { subscriberId: 'sub' },
    body: 'body',
    subject: 'subject',
    tags: [],
    data: {},
  };

  novu.notifications.cache.unshift(args, notification);
  novu.notifications.cache.unshift(args, notification);

  console.log(novu.notifications.cache.getAll(args).notifications.map(n => n.id));

End-to-end reproduction using @novu/react:

  1. In a React app that mounts <NovuProvider> and calls useNotifications({ archived: false, limit: 10 }), open the app in 2+ browser tabs so multiple Novu clients are alive for the same subscriber.
  2. Trigger a new in-app notification for that subscriber (via the Novu dashboard or the trigger API).
  3. Observe the rendered inbox in any tab: the new notification appears N times, where N is the number of currently mounted Novu clients.
  4. Refresh the page, the inbox re-renders from the HTTP response and the duplicates are gone… until the next notification arrives.
  5. React also emits the "Encountered two children with the same key" warning, because consumers naturally use notification.id as the list key

👍 Expected behavior

notifications.cache.unshift() should be idempotent with respect to notification.id within a given query scope. Calling it twice with the same notification should leave the cache holding exactly one entry with that id. As a consequence, cache.getAll() and useNotifications() should never return two items with the same id. A safe default would be to replace or skip any entry whose id already exists in the cached list before prepending.

👎 Actual Behavior with Screenshots

  • notifications.cache.unshift() should be idempotent with respect to notification.id within a given query scope
  • Calling it twice with the same notification should leave the cache holding exactly one entry with that id
  • As a consequence, cache.getAll() and useNotifications() should never return two items with the same id
  • A safe default would be to replace or skip any entry whose id already exists in the cached list before prepending

Novu version

3.14.2

npm version

10.9.4

node version

22.21.0

📃 Provide any additional context for the Bug.

No response

👀 Have you spent some time to check if this bug has been raised before?

  • I checked and didn't find a similar issue

🏢 Have you read the Contributing Guidelines?

Are you willing to submit PR?

None

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions