Repository: novuhq/novu
File: packages/react/src/hooks/useNotifications.ts (TypeScript)

Code before:
  const result = prev
    .map((notification) => {
      const updatedNotification = updatedMap.get(notification.id);
      if (updatedNotification) {
        return checkNotificationMatchesFilter(updatedNotification, filter) ? updatedNotification : null;
      }
      // Notification not in updated data - check if it still matches filter
      return checkNotificationMatchesFilter(notification, filter) ? notification : null;
    })
    .filter((n): n is Notification => n !== null);

  // Add new matching notifications that aren't already in the list
  // Insert at the beginning to match WebSocket behavior ([notification, ...prev])
  // This handles cases like: notification marked as read when we filter read: true -> add it
  const matchingNotifications = updated.filter((n) => checkNotificationMatchesFilter(n, filter));
  for (const notification of matchingNotifications) {
    if (!existingIds.has(notification.id)) {
      result.unshift(notification);
      existingIds.add(notification.id);
    }
  }

  return result;
}

export const useNotifications = (props?: UseNotificationsProps): UseNotificationsResult => {
  const {
    tags,
    data: dataFilter,
    read,
    archived = false,
    snoozed = false,
    seen,
    severity,
    limit,
    onSuccess,
    onError,
  } = props || {};
  const filterRef = useRef<NotificationFilter | undefined>(undefined);
  const { notifications, on } = useNovu();

  const getCurrentFilter = useCallback(
    () => filterRef.current || { tags, data: dataFilter, read, archived, snoozed, seen, severity },
    [tags, dataFilter, read, archived, snoozed, seen, severity]
  );
  const [data, setData] = useState<Array<Notification>>();
  const [error, setError] = useState<NovuError>();
  const [isLoading, setIsLoading] = useState(true);
  const [isFetching, setIsFetching] = useState(false);
  const [hasMore, setHasMore] = useState(false);
  const length = data?.length;

Review comment (performance):
I think that we should have a similar logic to what we do in the JS code, like in the `CountContext`. We not only update the state, but we also have some if statements, and update the cache with a new notification.

---

[Before I was thinking](https://novu.slack.com/archives/C06L08A4USH/p1753915931080109?thread_ts=1753033671.138029&cid=C06L08A4USH) to do that update automatically through the cache, but it will require more refactoring, and we also don't update the list of notifications when the Inbox is opened so this will be problematic, because we want to react for example to read/unread but not to new notification when the `notifications.list.updated` event is fired.

Code after:
        return;
      }

      // the event is called with the list of all notifications cached matching the current filter
      setData(data.notifications);
      setHasMore(data.hasMore);
    };

    const cleanup = novu.on('notifications.list.updated', listener);

    return () => {
      cleanup();
    };
  }, [filterRef, novu]);

  useWebSocketEvent({
    event: 'notifications.notification_received',
    eventHandler: ({ result: notification }) => {
      const currentFilter = filterRef.current;
      const matches = checkNotificationMatchesFilter(notification, currentFilter);
      if (matches) {
        // the limit and after props are used to create a cache key
        // the first batch of notifications in the cache doesn't include the after prop and we want to push to the first batch
        const cacheKey = { ...currentFilter, limit: limitRef.current };
        novu.notifications.cache.unshift(cacheKey, notification);
      }
    },
  });

  const fetchNotifications = useCallback(
    async (options?: { refetch: boolean }) => {
      if (options?.refetch) {
        setError(undefined);
        setIsLoading(true);
        setIsFetching(false);
      }
      setIsFetching(true);

      const response = await novu.notifications.list({
        ...filterRef.current,
        limit,
        after: options?.refetch ? undefined : afterRef.current,
      });

      if (response.error) {
        setError(response.error);
        onError?.(response.error);
        setIsLoading(false);
        setIsFetching(false);
      } else if (response.data) {
        const responseData = response.data;