Type-safe Server-Sent Events (SSE) hook for React with token authentication and retry logic
npm install sse-shared-worker-react-hookA TypeScript package for using Server-Sent Events (SSE) in React with full support for authentication tokens, retry logic, and type safety.
- ✅ Type-Safe: Fully written in TypeScript
- ✅ Token Authentication: Bearer token support
- ✅ Retry Logic: Retry with exponential backoff and max retry delay
- ✅ Customizable: Configurable for different needs
- ✅ React Hook: Easy to use with React Hooks
- ✅ Shared Worker Support: Shared Worker support for sharing a single connection across all tabs
``bash`
npm install sse-shared-worker-react-hookor
yarn add sse-shared-worker-react-hookor
pnpm install sse-shared-worker-react-hook
`tsx
import { useSSE } from 'sse-shared-worker-react-hook';
function MyComponent() {
const { status, lastEvent, events, error } = useSSE('/api/events');
if (status === 'connecting') {
return
if (error) {
return
return (
Status: {status}
Event count: {events.length}
{JSON.stringify(lastEvent.data, null, 2)}$3
`tsx
import { useSSE } from 'sse-shared-worker-react-hook';function AuthenticatedComponent() {
const token = 'your-auth-token';
const { status, lastEvent, events, error } = useSSE('/api/events', {
token,
maxRetries: 5,
maxRetryDelay: 30000, // 30 seconds
initialRetryDelay: 1000, // 1 second
});
return (
{/ ... /}
);
}
`$3
If you get "Failed to fetch" or CORS errors when connecting to a cross-origin SSE endpoint:
- Option 1: Use
credentials: 'same-origin' (default) or credentials: 'omit' so the browser does not send cookies; many CORS issues are caused by credential mode.
- Option 2: If you need cookies, set credentials: 'include' and ensure the server sends:
- Access-Control-Allow-Origin: (no * when using credentials)
- Access-Control-Allow-Credentials: true
- Option 3: Use a same-origin proxy so the browser only talks to your domain.Why CORS only fails when you send headers:
Sending custom headers (e.g.
Authorization) makes the request non-simple. The browser then sends a preflight OPTIONS request first. The server must:1. Respond to OPTIONS with status 2xx.
2. Include
Access-Control-Allow-Headers with the headers you send (e.g. Authorization), or *.
3. Include Access-Control-Allow-Origin (your origin or ; no if using credentials).If the server does not handle OPTIONS or does not allow your headers in
Access-Control-Allow-Headers, CORS fails only when you add headers; without headers the request stays "simple" and may succeed.`tsx
// Example: avoid credentials for cross-origin to reduce CORS strictness
useSSE('https://api.other-domain.com/events', {
credentials: 'omit',
headers: { Authorization: Bearer ${token} },
});
`$3
`tsx
import { useSSE } from 'sse-shared-worker-react-hook';interface NotificationData {
id: string;
message: string;
timestamp: number;
}
function TypedComponent() {
const { lastEvent, events } = useSSE('/api/notifications', {
token: 'your-token',
});
// lastEvent.data is now typed as NotificationData
return (
{events.map((event, index) => (
{event.data.message}
{new Date(event.data.timestamp).toLocaleString()}
))}
);
}
`$3
`tsx
import { useSSE } from 'sse-shared-worker-react-hook';interface MessageData {
text: string;
userId: string;
}
interface ErrorData {
code: string;
message: string;
}
// Define allowed event types
type EventTypes = 'message' | 'error' | 'update';
function TypedEventComponent() {
const { lastEvent, events } = useSSE(
'/api/events',
{
token: 'your-token',
}
);
// lastEvent.type is now typed as EventTypes
// TypeScript will enforce that only 'message' | 'error' | 'update' are valid
return (
{events.map((event, index) => {
if (event.type === 'message') {
// TypeScript knows event.data is MessageData here
return {event.data.text};
} else if (event.type === 'error') {
// TypeScript knows event.data is ErrorData here
return Error: {event.data.message};
}
return null;
})}
);
}
`$3
`tsx
import { useSSE } from 'sse-shared-worker-react-hook';function ControlledComponent() {
const { status, close, reconnect } = useSSE('/api/events', {
token: 'your-token',
autoReconnect: false, // Disable automatic reconnect
});
return (
Status: {status}
);
}
`$3
`tsx
import { useSSE } from 'sse-shared-worker-react-hook';function CustomHeadersComponent() {
const { status, lastEvent } = useSSE('/api/events', {
token: 'your-token',
headers: {
'X-Custom-Header': 'custom-value',
'X-Client-Version': '1.0.0',
},
});
return
{/ ... /};
}
`$3
`tsx
import { useSSE } from 'sse-shared-worker-react-hook';function CustomRetryComponent() {
const { status, retryCount } = useSSE('/api/events', {
token: 'your-token',
maxRetries: 10,
maxRetryDelay: 60000, // 60 seconds
retryDelayFn: (attempt) => {
// Custom logic: linear backoff
return attempt * 2000; // 2s, 4s, 6s, ...
},
});
return (
Status: {status}
Retry count: {retryCount}
);
}
`Using with Shared Worker
Benefits of using Shared Worker:
- ✅ Only one SSE connection in the entire system (not for each tab)
- ✅ Data is shared between all tabs and windows
- ✅ Reduced resource consumption and bandwidth
- ✅ Automatic synchronization between tabs
$3
First, you need to place the Shared Worker file in your project:
For Vite:
`ts
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { copyFileSync } from 'fs';
import { join } from 'path';export default defineConfig({
plugins: [
react(),
{
name: 'copy-shared-worker',
buildStart() {
copyFileSync(
join(__dirname, 'node_modules/sse-shared-worker-react-hook/public/shared-worker.js'),
join(__dirname, 'public/shared-worker.js')
);
},
},
],
});
`For Create React App:
Copy the Shared Worker file from the package to your
public folder:
`bash
cp node_modules/sse-shared-worker-react-hook/public/shared-worker.js public/shared-worker.js
`For Webpack:
`js
// webpack.config.js
const CopyWebpackPlugin = require('copy-webpack-plugin');module.exports = {
plugins: [
new CopyWebpackPlugin({
patterns: [
{
from: 'node_modules/sse-shared-worker-react-hook/public/shared-worker.js',
to: 'shared-worker.js',
},
],
}),
],
};
`$3
Shared Worker is not supported in:
- Internet Explorer (all versions)
- Safari iOS 7–15.8
- Some private or embedded browser contexts
- Non-HTTPS in some browsers
You can check support at runtime:
`tsx
import { isSharedWorkerSupported } from 'sse-shared-worker-react-hook';if (isSharedWorkerSupported()) {
// Use useSSEWithSharedWorker for shared connection across tabs
} else {
// Use useSSE for single-tab SSE (still works)
}
`To use Shared Worker when available and fall back to normal SSE otherwise (one hook, same API):
`tsx
import { useSSEAdaptive } from 'sse-shared-worker-react-hook';function AdaptiveComponent() {
const { status, lastEvent, events, error } = useSSEAdaptive(
'/api/events',
{ token: 'your-token' },
'/shared-worker.js'
);
// Same API as useSSE / useSSEWithSharedWorker — SSE works in both cases
return (
Status: {status}
{lastEvent && {JSON.stringify(lastEvent.data, null, 2)}}
);
}
`$3
`tsx
import { useSSEWithSharedWorker } from 'sse-shared-worker-react-hook';function SharedWorkerComponent() {
const { status, lastEvent, events, error } = useSSEWithSharedWorker(
'/api/events',
{
token: 'your-auth-token',
maxRetries: 5,
maxRetryDelay: 30000,
},
'/shared-worker.js' // Path to Shared Worker file
);
return (
Status: {status}
Event count: {events.length}
{lastEvent && (
Last event:
{JSON.stringify(lastEvent.data, null, 2)}
)}
);
}
`$3
`tsx
// Tab 1, Tab 2, Tab 3 - All use the same connection!// Tab 1
function Tab1Component() {
const { events } = useSSEWithSharedWorker('/api/notifications', {
token: localStorage.getItem('token'),
});
return (
Tab 1
{events.map((e, i) => (
{e.data.message}
))}
);
}// Tab 2 - Receives the same data!
function Tab2Component() {
const { events } = useSSEWithSharedWorker('/api/notifications', {
token: localStorage.getItem('token'),
});
return (
Tab 2
{events.map((e, i) => (
{e.data.message}
))}
);
}
`$3
`tsx
import { useSSEWithSharedWorker } from 'sse-shared-worker-react-hook';interface StockPrice {
symbol: string;
price: number;
change: number;
}
function StockTracker() {
const { lastEvent, events } = useSSEWithSharedWorker(
'/api/stock-prices',
{
token: 'your-token',
}
);
// lastEvent.data is typed as StockPrice
return (
{events.map((event, index) => (
{event.data.symbol}
Price: ${event.data.price}
Change: {event.data.change}%
))}
);
}
`$3
`tsx
import { useSSEWithSharedWorker } from 'sse-shared-worker-react-hook';interface NotificationData {
title: string;
body: string;
}
type NotificationEventTypes = 'notification' | 'alert' | 'system';
function NotificationComponent() {
const { lastEvent, events } = useSSEWithSharedWorker<
NotificationData,
NotificationEventTypes
>('/api/notifications', {
token: 'your-token',
});
// event.type is now typed as NotificationEventTypes
return (
{events.map((event, index) => (
Type: {event.type}
{event.data.title}
{event.data.body}
))}
);
}
`API Reference
$3
A React Hook for connecting to an SSE endpoint (each component has a separate connection).
#### Generic Parameters
-
T - Type of the event data (default: any)
- K - Type of the event type (default: string)#### Parameters
-
url: string | null - URL endpoint for SSE
- options?: SSEOptions - Configuration options#### Returns
SSEReturn - See below for details.$3
A React Hook for connecting to an SSE endpoint using Shared Worker (one connection for all tabs).
#### Generic Parameters
-
T - Type of the event data (default: any)
- K - Type of the event type (default: string)#### Parameters
-
url: string | null - URL endpoint for SSE
- options?: SSEOptions - Configuration options
- workerPath?: string - Path to Shared Worker file (default: '/shared-worker.js')#### Returns
SSEReturn - See below for details.$3
A React Hook that uses Shared Worker when supported and falls back to normal SSE otherwise. Same API as
useSSE / useSSEWithSharedWorker.#### Generic Parameters
-
T - Type of the event data (default: any)
- K - Type of the event type (default: string)#### Parameters
-
url: string | null - URL endpoint for SSE
- options?: SSEOptions - Configuration options
- workerPath?: string - Path to Shared Worker file (default: '/shared-worker.js')#### Returns
SSEReturn - See below for details.$3
Returns
true if the environment supports Shared Worker (browser, window.SharedWorker defined). Use this to branch logic or to decide whether to use useSSEWithSharedWorker vs useSSE. For a single hook that auto-fallbacks, use useSSEAdaptive.$3
`typescript
interface SSEReturn {
status: SSEStatus; // Connection status
lastEvent: SSEEvent | null; // Last received event
events: SSEEvent[]; // All received events
error: Error | null; // Error (if any)
close: () => void; // Close connection
reconnect: () => void; // Reconnect
retryCount: number; // Retry attempt count
}
`$3
`typescript
interface SSEOptions {
token?: string; // Bearer token for authentication
maxRetryDelay?: number; // Maximum retry delay (ms) - default: 30000
initialRetryDelay?: number; // Initial retry delay (ms) - default: 1000
maxRetries?: number; // Maximum retry count - default: 5
headers?: Record; // Additional headers
autoReconnect?: boolean; // Auto reconnect - default: true
retryDelayFn?: (attempt: number) => number; // Retry delay calculation function
}
`$3
`typescript
type SSEStatus =
| 'connecting' // Connecting
| 'connected' // Connected
| 'disconnected' // Disconnected
| 'error' // Error
| 'closed'; // Closed
`$3
`typescript
interface SSEEvent {
type: K; // Event type (type-safe with generic K)
data: T; // Event data (type-safe with generic T)
id?: string; // Event ID (from server)
timestamp: number; // Receive timestamp
}
`Example:
`typescript
// Without generics (defaults to any, string)
const event: SSEEvent = {
type: 'message',
data: { text: 'Hello' },
timestamp: Date.now()
};// With data type only
const typedEvent: SSEEvent<{ text: string }> = {
type: 'message',
data: { text: 'Hello' },
timestamp: Date.now()
};
// With both data and event type
type EventTypes = 'message' | 'error' | 'update';
const fullyTypedEvent: SSEEvent<{ text: string }, EventTypes> = {
type: 'message', // TypeScript enforces: must be 'message' | 'error' | 'update'
data: { text: 'Hello' },
timestamp: Date.now()
};
`Advanced Examples
$3
`tsx
import { useEffect } from 'react';
import { useSSE } from 'sse-shared-worker-react-hook';interface UserStatus {
userId: string;
online: boolean;
}
function UserStatusComponent() {
const [users, setUsers] = useState
useEffect(() => {
if (lastEvent) {
setUsers((prev) => {
const next = new Map(prev);
next.set(lastEvent.data.userId, lastEvent.data.online);
return next;
});
}
}, [lastEvent]);
return (
{Array.from(users.entries()).map(([userId, online]) => (
User {userId}: {online ? 'Online' : 'Offline'}
))}
);
}
`$3
`tsx
import { useSSE } from 'sse-shared-worker-react-hook';function ErrorHandlingComponent() {
const { status, error, retryCount, reconnect } = useSSE('/api/events', {
token: 'your-token',
maxRetries: 3,
});
if (status === 'error' && retryCount >= 3) {
return (
Connection failed. Please try again.
);
} return
{/ ... /};
}
`Important Notes
1. Token Authentication: When using
token, the package uses the fetch API (because EventSource doesn't support custom headers).2. Retry Logic: Retry is performed with exponential backoff. You can define your own logic with
retryDelayFn.3. Memory Management: Events are stored in the
events array. To prevent excessive memory usage, you can periodically clear the array. The package automatically limits events to 100 items.4. Cleanup: The connection is automatically closed when the component unmounts.
5. Shared Worker:
- To use
useSSEWithSharedWorker, you must place the Shared Worker file in the public` folderMIT