notiflowsDocs
SDKsClient-side

React

Notiflows React SDK with hooks and components for notification UI

The React SDK (@notiflows/react) provides React hooks and pre-built components for displaying notifications in your application. It's built on top of the JavaScript SDK.

Installation

npm install @notiflows/react
# or
pnpm add @notiflows/react
# or
yarn add @notiflows/react

Quick Start

Import the styles and wrap your application with NotiflowsProvider:

import '@notiflows/react/styles.css';
import {
  NotiflowsProvider,
  FeedRoot,
  FeedTrigger,
  FeedContent,
} from '@notiflows/react';

function App() {
  return (
    <NotiflowsProvider
      apiKey="pk_your_public_key"
      userId="user_123"
      userKey="jwt_token_from_backend"
      channelId="your-channel-id"
    >
      <Header />
      <MainContent />
    </NotiflowsProvider>
  );
}

function Header() {
  return (
    <header>
      <nav>
        <FeedRoot>
          <FeedTrigger />
          <FeedContent />
        </FeedRoot>
      </nav>
    </header>
  );
}

That's it! You now have a fully functional notification bell with a popover feed.

Styles

Default Styles

The SDK ships with a CSS file that provides ready-to-use styling:

import '@notiflows/react/styles.css';

You must import the styles for the pre-built components to render correctly. Import it once in your app's entry point.

Customization with CSS Variables

Override CSS variables to match your brand:

:root {
  /* Colors */
  --nf-primary: #0066cc;
  --nf-primary-foreground: #ffffff;
  --nf-background: #ffffff;
  --nf-foreground: #0f0f0f;
  --nf-muted: #f5f5f5;
  --nf-muted-foreground: #737373;
  --nf-border: #e5e5e5;
  --nf-popover: #ffffff;
  --nf-popover-foreground: #0f0f0f;

  /* Typography */
  --nf-font-family: system-ui, sans-serif;
  --nf-font-size-xs: 0.75rem;
  --nf-font-size-sm: 0.875rem;
  --nf-font-size-base: 1rem;

  /* Spacing & Radius */
  --nf-radius: 0.5rem;
  --nf-radius-sm: 0.25rem;

  /* Shadows */
  --nf-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
  --nf-shadow-md: 0 10px 15px -3px rgb(0 0 0 / 0.1);

  /* Special */
  --nf-unread-indicator: #0891b2;
  --nf-badge-bg: #ef4444;
  --nf-badge-text: #ffffff;
}

Dark Mode

Add the dark class to a parent element to enable dark mode:

<html class="dark">
  <!-- Components automatically use dark colors -->
</html>

The SDK includes dark mode styles that are activated when a parent element has the dark class.

Custom Classes

All components accept a className prop for additional styling:

<FeedContent className="my-custom-popover" />
<FeedPanel className="my-custom-panel" />

Provider

The NotiflowsProvider initializes the client and provides context to all child components.

<NotiflowsProvider
  // Required
  apiKey="pk_your_public_key"
  userId="user_123"
  userKey="jwt_token_from_backend"
  channelId="your-channel-id"

  // Optional
  apiUrl="https://api.notiflows.com/user/v1"
  wsUrl="wss://api.notiflows.com/ws/v1"
>
  {children}
</NotiflowsProvider>

The userKey must be generated on your backend using your Application Signing Key. Never expose your signing key in client-side code. See Generating User Tokens for instructions.

Components

Feed Components

The feed components create a popover-based notification center:

import {
  FeedRoot,
  FeedTrigger,
  FeedContent,
  FeedPanel,
  BellButton,
} from '@notiflows/react';

// Minimal setup - uses defaults
<FeedRoot>
  <FeedTrigger />   {/* Renders BellButton by default */}
  <FeedContent />   {/* Renders FeedPanel by default */}
</FeedRoot>

// Explicit setup
<FeedRoot>
  <FeedTrigger>
    <BellButton />
  </FeedTrigger>
  <FeedContent>
    <FeedPanel />
  </FeedContent>
</FeedRoot>

FeedRoot

Wrapper component that manages the popover state. Wraps Radix UI Popover.

<FeedRoot
  open={boolean}           // Controlled open state
  onOpenChange={fn}        // Callback when open state changes
  defaultOpen={boolean}    // Initial open state (uncontrolled)
>
  {children}
</FeedRoot>

FeedTrigger

Button that opens the notification popover. Renders BellButton by default.

// Default
<FeedTrigger />

// Custom trigger
<FeedTrigger>
  <button className="my-button">
    <MyBellIcon />
    <Badge />
  </button>
</FeedTrigger>

FeedContent

Popover content container. Renders FeedPanel by default.

// Default
<FeedContent />

// With custom alignment
<FeedContent align="end" sideOffset={8} />

// With custom content
<FeedContent>
  <FeedPanel NotificationComponent={MyNotification} />
</FeedContent>

FeedPanel

The main notification panel with tabs (All, Unread, Read) and settings.

<FeedPanel
  NotificationComponent={CustomNotification}  // Custom notification renderer
  className="my-panel"
/>

BellButton

Bell icon button with unread badge:

import { BellButton, Badge } from '@notiflows/react';

// Standalone usage
<BellButton onClick={handleClick} />

// Just the badge
<Badge totalUnread={5} />

Notification

Individual notification item component:

import { Notification } from '@notiflows/react';

// Default rendering
<Notification item={entry} />

// With custom action handler
<Notification
  item={entry}
  onActionClick={(url, entry) => {
    // Custom navigation logic
    router.push(url);
  }}
/>

MarkAllAsRead

Button to mark all notifications as read:

import { MarkAllAsRead } from '@notiflows/react';

<MarkAllAsRead onClick={() => markAllAsRead()}>
  Mark all as read
</MarkAllAsRead>

MarkAsArchived

Archive button for individual notifications:

import { MarkAsArchived } from '@notiflows/react';

<MarkAsArchived item={entry} />

Preferences

Preferences panel with toggles for each notiflow:

import { Preferences } from '@notiflows/react';

<Preferences />

Avatar

User avatar with fallback:

import { Avatar } from '@notiflows/react';

<Avatar
  src="https://example.com/avatar.jpg"
  alt="John Doe"
  fallback="JD"
/>

Hooks

useNotiflows

Access the Notiflows client and feed client:

import { useNotiflows } from '@notiflows/react';

function MyComponent() {
  const { client, feedClient } = useNotiflows();

  // Direct API access
  const handleGetPrefs = async () => {
    const prefs = await client.userPreferences().get();
    console.log(prefs);
  };
}

useFeed

Access the notification feed with automatic real-time updates:

import { useFeed } from '@notiflows/react';

function MyComponent() {
  const {
    entries,         // FeedEntry[] - notification items
    totalUnread,     // number - unread count
    totalUnseen,     // number - unseen count
    lastPage,        // pagination info
    isLoading,       // boolean - initial load
    isLoadingMore,   // boolean - loading more items
    error,           // NotiflowsApiError | null
    loadMore,        // () => Promise<void>
    clearError,      // () => void
    retry,           // () => void
  } = useFeed();

  // With options
  const { entries } = useFeed({
    status: FeedEntryStatus.Unread,
    notiflow: 'order-updates',  // filter by notiflow handle
    topic: 'order:123',         // filter by topic
    archived: true,             // include archived entries
    limit: 20,
  });

  return (
    <div>
      <p>{totalUnread} unread notifications</p>
      {entries.map(entry => (
        <div key={entry.id}>{entry.data.body}</div>
      ))}
      {lastPage?.has_more_after && (
        <button onClick={loadMore} disabled={isLoadingMore}>
          Load more
        </button>
      )}
    </div>
  );
}

useNotificationStatus

Update notification states:

import { useNotificationStatus } from '@notiflows/react';

function MyComponent() {
  const {
    markAsSeen,        // (entryId: string) => Promise<void>
    markAsRead,        // (entryId: string) => Promise<void>
    markAsClicked,     // (entryId: string) => Promise<void>
    markAsArchived,    // (entryId: string) => Promise<void>
    markAsUnarchived,  // (entryId: string) => Promise<void>
    markAllAsRead,     // () => Promise<void>
    batchMarkAsRead,   // (entryIds: string[]) => Promise<void>
    batchMarkAsArchived, // (entryIds: string[]) => Promise<void>
  } = useNotificationStatus();

  const handleNotificationClick = async (entry: FeedEntry) => {
    if (!entry.read_at) {
      await markAsRead(entry.id);
    }
    if (entry.data.action_type === 'default' && entry.data.action_url) {
      window.location.href = entry.data.action_url;
    }
  };
}

usePreferences

Manage user notification preferences:

import { usePreferences } from '@notiflows/react';

function PreferencesPanel() {
  const {
    preferences,                // Preferences | null
    isLoading,                  // boolean
    updateNotiflowPreferences,  // (notiflowHandle, prefs) => Promise<void>
  } = usePreferences();

  if (isLoading) return <Spinner />;

  return (
    <div>
      {Object.entries(preferences?.notiflows || {}).map(([handle, pref]) => (
        <label key={handle}>
          <input
            type="checkbox"
            checked={pref.enabled}
            onChange={() =>
              updateNotiflowPreferences(handle, {
                ...pref,
                enabled: !pref.enabled,
              })
            }
          />
          {pref.name}
        </label>
      ))}
    </div>
  );
}

Building Custom UI

Use the hooks to build completely custom notification UIs:

import { useFeed, useNotificationStatus } from '@notiflows/react';

function CustomNotificationCenter() {
  const { entries, totalUnread, isLoading, loadMore } = useFeed();
  const { markAsRead, markAllAsRead } = useNotificationStatus();

  if (isLoading) {
    return <div className="loading">Loading...</div>;
  }

  return (
    <div className="notification-center">
      <header>
        <h2>Notifications</h2>
        <span className="badge">{totalUnread}</span>
        <button onClick={markAllAsRead}>Mark all read</button>
      </header>

      <ul className="notification-list">
        {entries.map(entry => (
          <li
            key={entry.id}
            className={entry.read_at ? 'read' : 'unread'}
            onClick={() => markAsRead(entry.id)}
          >
            {entry.actor && (
              <img
                src={entry.actor.avatar}
                alt={entry.actor.first_name}
              />
            )}
            <div>
              <p>{entry.data.body}</p>
              <time>{formatTime(entry.created_at)}</time>
            </div>
          </li>
        ))}
      </ul>

      <button onClick={loadMore}>Load more</button>
    </div>
  );
}

TypeScript

The SDK is fully typed. Import types as needed:

import type {
  FeedEntry,
  FeedEntryCollection,
  FeedEntryStatus,
  Preferences,
  NotiflowsApiError,
} from '@notiflows/react';

// Re-exported from @notiflows/client
import {
  isApiError,
  BadRequestError,
  UnauthenticatedError,
  NotFoundError,
} from '@notiflows/react';

Error Handling

Handle errors from hooks:

import { useFeed, isApiError } from '@notiflows/react';

function NotificationList() {
  const { entries, error, retry, clearError } = useFeed();

  if (error) {
    return (
      <div className="error">
        <p>Failed to load notifications</p>
        {isApiError(error) && <p>{error.message}</p>}
        <button onClick={retry}>Retry</button>
        <button onClick={clearError}>Dismiss</button>
      </div>
    );
  }

  return (
    <ul>
      {entries.map(entry => (
        <li key={entry.id}>{entry.data.body}</li>
      ))}
    </ul>
  );
}

Next.js Integration

App Router

Use the 'use client' directive for components that use the SDK:

// components/notifications.tsx
'use client';

import '@notiflows/react/styles.css';
import {
  NotiflowsProvider,
  FeedRoot,
  FeedTrigger,
  FeedContent,
} from '@notiflows/react';

interface NotificationsProps {
  userKey: string;
  userId: string;
}

export function Notifications({ userKey, userId }: NotificationsProps) {
  return (
    <NotiflowsProvider
      apiKey={process.env.NEXT_PUBLIC_NOTIFLOWS_API_KEY!}
      channelId={process.env.NEXT_PUBLIC_NOTIFLOWS_CHANNEL_ID!}
      userKey={userKey}
      userId={userId}
    >
      <FeedRoot>
        <FeedTrigger />
        <FeedContent />
      </FeedRoot>
    </NotiflowsProvider>
  );
}
// app/layout.tsx
import { Notifications } from '@/components/notifications';
import { auth, generateUserKey } from '@/lib/auth';

export default async function RootLayout({ children }) {
  const session = await auth();

  // Generate user key on the server
  const userKey = await generateUserKey(session.user.id);

  return (
    <html lang="en">
      <body>
        <nav>
          <Notifications
            userKey={userKey}
            userId={session.user.id}
          />
        </nav>
        {children}
      </body>
    </html>
  );
}

Pages Router

// pages/_app.tsx
import '@notiflows/react/styles.css';

// components/notifications.tsx
import {
  NotiflowsProvider,
  FeedRoot,
  FeedTrigger,
  FeedContent,
} from '@notiflows/react';

export function Notifications({ userKey, userId }) {
  return (
    <NotiflowsProvider
      apiKey={process.env.NEXT_PUBLIC_NOTIFLOWS_API_KEY}
      channelId={process.env.NEXT_PUBLIC_NOTIFLOWS_CHANNEL_ID}
      userKey={userKey}
      userId={userId}
    >
      <FeedRoot>
        <FeedTrigger />
        <FeedContent />
      </FeedRoot>
    </NotiflowsProvider>
  );
}

Allowed Origins

Before using the SDK in a browser, you must add your domain to the project's allowed origins list. Without this, the browser will block all API requests due to CORS.

See Allowed Origins for setup instructions.

Browser Support

The SDK supports all modern browsers (ES2022+):

  • Chrome 94+
  • Firefox 93+
  • Safari 15+
  • Edge 94+

Next Steps

On this page