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

Code before:
    [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;
  const after = length ? data[length - 1].id : undefined;

  useEffect(() => {
    const listener = ({
      data: updatedData,
    }: {
      data: { notifications: Notification[]; hasMore: boolean; filter: NotificationFilter };
    }) => {
      if (!updatedData) return;

      const currentFilter = getCurrentFilter();
      const filterMatches = isSameFilter(currentFilter, updatedData.filter);

      // Always merge - mergeNotifications handles filtering correctly
      setData((prev) => mergeNotifications(prev, updatedData.notifications, currentFilter, filterMatches));

      // Only update hasMore when filter matches exactly (preserves pagination)
      if (filterMatches) {
        setHasMore(updatedData.hasMore);
      }
    };

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

    return () => {
      cleanup();
    };
  }, [getCurrentFilter, on]);

  useWebSocketEvent({
    event: 'notifications.notification_received',
    eventHandler: ({ result: notification }) => {
      const currentFilter = getCurrentFilter();
      const matches = checkNotificationMatchesFilter(notification, currentFilter);
      if (matches) {
        setData((prev) => {
          // Prevent duplicates - mergeNotifications will handle this when notifications.list.updated fires
          if (prev?.some((n) => n.id === notification.id)) {
            return prev;
          }
          return prev ? [notification, ...prev] : [notification];
        });
      }

Review comment (performance):
If the filter matches, there should be no need to merge notifications and checking with `checkNotificationMatchesFilter` function because the `notifications.list.updated` event is fired with notifications that are stored by a cache key that consists of the filter. We should be safe just to mutate the state.

Code after:

      if (response.error) {
        setError(response.error);
        onError?.(response.error);
        setIsLoading(false);
        setIsFetching(false);
      } else if (response.data) {
        const responseData = response.data;
        onSuccess?.(responseData.notifications);
        setData(responseData.notifications);
        setHasMore(responseData.hasMore);
        setIsLoading(false);
        setIsFetching(false);
      }
    },
    [novu, filterRef, afterRef, limit, onError, onSuccess]
  );

  useEffect(() => {
    novu.notifications.clearCache({ filter: filterRef.current });
    fetchNotifications({ refetch: true });
  }, [filterRef, novu, JSON.stringify(filterRef.current), fetchNotifications]);

  const refetch = useCallback(() => {
    novu.notifications.clearCache({ filter: filterRef.current });
    return fetchNotifications({ refetch: true });
  }, [filterRef, novu, fetchNotifications]);

  const fetchMore = useCallback(async () => {
    if (!hasMore || isFetching) return;

    return fetchNotifications();
  }, [hasMore, isFetching, fetchNotifications]);

  const readAll = useCallback(async () => {
    return await novu.notifications.readAll({ tags: filterRef.current.tags, data: filterRef.current.data });
  }, [filterRef, novu]);

  const seenAll = useCallback(async () => {
    return await novu.notifications.seenAll({ tags: filterRef.current.tags, data: filterRef.current.data });
  }, [filterRef, novu]);

  const archiveAll = useCallback(async () => {
    return await novu.notifications.archiveAll({ tags: filterRef.current.tags, data: filterRef.current.data });
  }, [filterRef, novu]);

  const archiveAllRead = useCallback(async () => {
    return await novu.notifications.archiveAllRead({ tags: filterRef.current.tags, data: filterRef.current.data });
  }, [filterRef, novu]);

  return {