@fibersse/react

April 3, 2026 · View on GitHub

React hooks for fibersse — replace polling with SSE cache invalidation in one line.

Install

npm install @fibersse/react

Peer dependency: React 18+

Quick Start

import { useSSEInvalidation } from '@fibersse/react';
import { useQueryClient } from '@tanstack/react-query';

function App() {
  const queryClient = useQueryClient();

  const { connected } = useSSEInvalidation({
    topics: ['orders', 'products', 'dashboard'],
    onInvalidate: (resource, action, id) => {
      queryClient.invalidateQueries({ queryKey: [resource] });
      if (id) queryClient.invalidateQueries({ queryKey: [resource, id] });
    },
    onProgress: (id, current, total, pct) => {
      console.log(`Import ${id}: ${pct}%`);
    },
    onComplete: (id, status) => {
      if (status === 'completed') toast.success('Done!');
    },
  });

  return <span>{connected ? 'Live' : 'Connecting...'}</span>;
}

That's it. No setInterval. No polling. Your queries refetch only when data actually changes.

Hooks

The primary hook for replacing polling. Connects to a fibersse hub and routes events to your cache layer.

const { connected, connectionId, disconnect, reconnect } = useSSEInvalidation({
  // Required
  topics: ['orders', 'dashboard'],

  // Cache invalidation (the main thing)
  onInvalidate: (resource, action, resourceId, hint) => {
    queryClient.invalidateQueries({ queryKey: [resource] });
  },

  // Batch invalidation (multiple resources in one event)
  onBatch: (events) => {
    events.forEach(e => queryClient.invalidateQueries({ queryKey: [e.resource] }));
  },

  // Progress tracking (coalesced — 1000 updates → ~15 events)
  onProgress: (resourceId, current, total, pct, hint) => {
    setProgress(pct);
  },

  // Completion signals
  onComplete: (resourceId, status, hint) => {
    if (status === 'completed') refetchEverything();
  },

  // Generic refresh signals
  onSignal: () => {
    queryClient.invalidateQueries();
  },

  // Optional config
  url: '/api/v1/events/stream',       // SSE endpoint (default)
  ticketUrl: '/api/sse/ticket',        // Ticket endpoint (default)
  enabled: isAuthenticated,             // Connect only when ready
  visibilityAware: true,                // Disconnect on hidden tab (default)
  maxReconnectAttempts: 8,              // Exponential backoff
});

useSSE — Low-Level

Full control over event handling. Use when you need custom event types beyond invalidation.

const { connected } = useSSE({
  topics: ['notifications', 'live'],
  onEvent: {
    notification: (data) => showToast(data),
    live_visitors: (data) => setVisitors(data),
    intelligence: (data) => setSnapshot(data),
  },
  onConnect: ({ connection_id, topics }) => {
    console.log('Connected:', connection_id);
  },
  onServerShutdown: () => {
    console.log('Server draining, will reconnect...');
  },
});

With SWR

import { useSWRConfig } from 'swr';

const { mutate } = useSWRConfig();

useSSEInvalidation({
  topics: ['orders'],
  onInvalidate: (resource) => mutate(`/api/${resource}`),
});

Features

  • Ticket authentication — POST for a one-time ticket, then connect with it (EventSource can't send headers)
  • Exponential backoff — 3s → 6s → 12s → ... → 60s cap, with jitter
  • Visibility-aware — disconnects when tab is hidden, reconnects when visible
  • Type-safe — full TypeScript types for all event payloads
  • Zero runtime dependencies — only React as a peer dependency
  • Framework agnostic — works with TanStack Query, SWR, Zustand, Redux, or plain setState

Auth Flow

1. Client calls POST /api/sse/ticket (with JWT cookie/header)
2. Server returns { ticket: "one-time-token", topics: [...] }
3. Client opens EventSource at /events?ticket=TOKEN&topics=orders,dashboard
4. On disconnect/error → exponential backoff → get new ticket → reconnect

License

MIT — Vinod Morya