# Notification list

> The caller's latest notifications with an unread marker, loaded again on every change.

Source: https://bettersupabase.com/docs/registry/ui-recipes/notification-list

{/* Generated by scripts/build-recipes.ts from the example apps. Edit the examples or the script, then run `pnpm recipes`. */}

Both sides call `useNotifications` from `better-supabase/blocks/notifications/react`; the [notifications block](https://bettersupabase.com/blocks/notifications) must be installed (`npx better-supabase add notifications`). Its page on bettersupabase.com is [/recipes/notification-list](https://bettersupabase.com/recipes/notification-list).

Web, with shadcn components:

```bash
npx shadcn add badge empty skeleton
```

```bash
npx shadcn add @better-supabase/notifications-actions-next
```

```tsx title="apps/examples/nextjs/src/features/notifications/components/notification-list.tsx"
"use client";

import { useNotifications } from "better-supabase/blocks/notifications/react";
import { BellIcon } from "lucide-react";
import { useExtracted, useFormatter } from "next-intl";

import { Badge } from "@/components/ui/badge";
import {
  Empty,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty";
import { Skeleton } from "@/components/ui/skeleton";
import { useAuth } from "@/lib/hooks";

import type { NotificationRow } from "../notification-types";

import { loadNotifications } from "../notifications-actions";

async function loadRows(): Promise<readonly NotificationRow[]> {
  const result = await loadNotifications({});

  return result.ok ? result.data : [];
}

/**
 * The caller's latest notifications, loaded again after each broadcast on their
 * private topic `notifications:<user id>`: a new one, or one marked read.
 */
export function NotificationList() {
  const t = useExtracted("notifications");
  const format = useFormatter();
  const auth = useAuth();
  const userId = auth.user?.id;

  const { items } = useNotifications({
    topic: userId ? `notifications:${userId}` : null,
    load: loadRows,
  });

  if (items === undefined) {
    return <NotificationListSkeleton />;
  }

  if (items.length === 0) {
    return (
      <Empty className="border">
        <EmptyHeader>
          <EmptyMedia variant="icon">
            <BellIcon />
          </EmptyMedia>
          <EmptyTitle>{t("Nothing here yet")}</EmptyTitle>
          <EmptyDescription>
            {t("Send yourself a notification to see it arrive.")}
          </EmptyDescription>
        </EmptyHeader>
      </Empty>
    );
  }

  return (
    <ul className="divide-y rounded-xl border">
      {items.map((notification) => (
        <li
          key={notification.id}
          className="flex items-center gap-3 px-4 py-3 text-sm"
        >
          <span className="flex-1">{notification.title}</span>
          {notification.readAt ? null : <Badge>{t("New")}</Badge>}
          <span className="text-xs text-muted-foreground tabular-nums">
            {format.dateTime(new Date(notification.createdAt), {
              dateStyle: "medium",
              timeStyle: "short",
            })}
          </span>
        </li>
      ))}
    </ul>
  );
}

function NotificationListSkeleton() {
  return (
    <div className="space-y-2" aria-busy="true">
      {Array.from({ length: 4 }, (_, index) => (
        <Skeleton key={index} className="h-11 w-full" />
      ))}
    </div>
  );
}
```

React Native, with `ActivityIndicator`, `FlatList`, `Text`, `View` from `react-native`:

```bash
npx shadcn add @better-supabase/blocks
```

```tsx title="apps/examples/expo-powersync/src/components/notification-list.tsx"
import { useNotifications } from "better-supabase/blocks/notifications/react";
import { tenantOf, useAuth, useSupabase } from "better-supabase/react";
import { ActivityIndicator, FlatList, Text, View } from "react-native";

import { blocks } from "../lib/blocks";

/**
 * The caller's latest notifications, loaded again after each broadcast on their
 * private topic `notifications:<user id>`: a new one, or one marked read.
 */
export function NotificationList() {
  const auth = useAuth();
  const supabase = useSupabase();
  const tenant = tenantOf(auth);
  const userId = auth.user?.id;

  const { items } = useNotifications({
    topic: userId ? `notifications:${userId}` : null,
    load: async () => {
      if (!tenant) {
        return [];
      }

      const result = await blocks(supabase).notifications.list({
        tenant,
        limit: 20,
      });

      return result.ok ? result.data : [];
    },
  });

  if (items === undefined) {
    return <ActivityIndicator />;
  }

  return (
    <FlatList
      data={items}
      keyExtractor={(item) => item.id}
      ListEmptyComponent={<Text>Nothing here yet</Text>}
      renderItem={({ item }) => (
        <View style={{ flexDirection: "row", gap: 8, paddingVertical: 12 }}>
          <Text style={{ flex: 1 }}>{item.data.title}</Text>
          {item.readAt ? null : <Text style={{ fontWeight: "600" }}>New</Text>}
          <Text style={{ color: "#737373" }}>
            {item.createdAt.toLocaleString(undefined, {
              dateStyle: "medium",
              timeStyle: "short",
            })}
          </Text>
        </View>
      )}
    />
  );
}
```