React Hooks library for remote data fetching with stale-while-revalidate caching strategy
—
The useSWRSubscription hook enables real-time data updates through WebSockets, Server-Sent Events, or other subscription mechanisms.
Hook for real-time data updates through subscription mechanisms.
/**
* Hook for real-time data updates through subscription mechanisms
* @param key - Unique identifier for the subscription
* @param subscribe - Function that sets up the subscription
* @param config - Configuration options for the subscription
* @returns SWRSubscriptionResponse with current data and error state
*/
function useSWRSubscription<Data = any, Error = any>(
key: Key,
subscribe: SWRSubscription<Key, Data, Error>,
config?: SWRConfiguration<Data, Error>
): SWRSubscriptionResponse<Data, Error>;Usage Examples:
import useSWRSubscription from "swr/subscription";
// WebSocket subscription
const { data, error } = useSWRSubscription(
"ws://localhost:3001",
(key, { next }) => {
const socket = new WebSocket(key);
socket.addEventListener("message", (event) => {
next(null, JSON.parse(event.data));
});
socket.addEventListener("error", (event) => {
next(event);
});
// Return cleanup function
return () => socket.close();
}
);
// Server-Sent Events
const { data } = useSWRSubscription(
"/api/events",
(key, { next }) => {
const eventSource = new EventSource(key);
eventSource.onmessage = (event) => {
next(null, JSON.parse(event.data));
};
eventSource.onerror = (error) => {
next(error);
};
return () => eventSource.close();
}
);
// Firebase Realtime Database
const { data } = useSWRSubscription(
["firebase", userId],
([, userId], { next }) => {
const ref = firebase.database().ref(`users/${userId}`);
const listener = ref.on("value", (snapshot) => {
next(null, snapshot.val());
}, (error) => {
next(error);
});
return () => ref.off("value", listener);
}
);The return value from useSWRSubscription with real-time data and error state.
interface SWRSubscriptionResponse<Data, Error> {
/** The current data from the subscription (undefined if no data received) */
data?: Data;
/** Error from the subscription (undefined if no error) */
error?: Error;
}Function that sets up and manages the subscription lifecycle.
type SWRSubscription<SWRSubKey, Data, Error> = (
key: SWRSubKey,
options: SWRSubscriptionOptions<Data, Error>
) => (() => void) | void;
interface SWRSubscriptionOptions<Data, Error> {
/** Function to call with new data or errors */
next: (error?: Error | null, data?: Data | MutatorCallback<Data>) => void;
}
type MutatorCallback<Data> = (currentData: Data | undefined) => Data | undefined;Subscription Function Examples:
// WebSocket with reconnection
const webSocketSubscription = (url: string, { next }: SWRSubscriptionOptions<any, Error>) => {
let socket: WebSocket;
let reconnectTimer: NodeJS.Timeout;
const connect = () => {
socket = new WebSocket(url);
socket.onopen = () => {
console.log("WebSocket connected");
};
socket.onmessage = (event) => {
next(null, JSON.parse(event.data));
};
socket.onclose = () => {
console.log("WebSocket disconnected, reconnecting...");
reconnectTimer = setTimeout(connect, 3000);
};
socket.onerror = (error) => {
next(new Error("WebSocket error"));
};
};
connect();
// Cleanup function
return () => {
clearTimeout(reconnectTimer);
socket?.close();
};
};
// Socket.io subscription
const socketIOSubscription = (event: string, { next }: SWRSubscriptionOptions<any, Error>) => {
const socket = io();
socket.on(event, (data) => {
next(null, data);
});
socket.on("error", (error) => {
next(error);
});
return () => {
socket.off(event);
socket.disconnect();
};
};
// GraphQL subscription (with graphql-ws)
const graphqlSubscription = (query: string, { next }: SWRSubscriptionOptions<any, Error>) => {
const client = createClient({
url: "ws://localhost:4000/graphql",
});
const unsubscribe = client.subscribe(
{ query },
{
next: (data) => next(null, data),
error: (error) => next(error),
complete: () => console.log("Subscription completed"),
}
);
return unsubscribe;
};
// Polling subscription (alternative to refreshInterval)
const pollingSubscription = (url: string, { next }: SWRSubscriptionOptions<any, Error>) => {
const poll = async () => {
try {
const response = await fetch(url);
const data = await response.json();
next(null, data);
} catch (error) {
next(error as Error);
}
};
// Initial poll
poll();
// Set up interval
const intervalId = setInterval(poll, 5000);
return () => clearInterval(intervalId);
};
// EventSource with custom headers
const eventSourceSubscription = (url: string, { next }: SWRSubscriptionOptions<any, Error>) => {
const eventSource = new EventSource(url);
eventSource.addEventListener("data", (event) => {
next(null, JSON.parse(event.data));
});
eventSource.addEventListener("error", (error) => {
next(new Error("EventSource error"));
});
// Handle specific event types
eventSource.addEventListener("user-update", (event) => {
next(null, { type: "user-update", data: JSON.parse(event.data) });
});
return () => eventSource.close();
};Common patterns for complex real-time scenarios.
Chat Application:
interface ChatMessage {
id: string;
user: string;
message: string;
timestamp: number;
}
function ChatRoom({ roomId }: { roomId: string }) {
const { data: messages, error } = useSWRSubscription(
roomId ? `ws://localhost:3001/chat/${roomId}` : null,
(key, { next }) => {
const socket = new WebSocket(key);
socket.onmessage = (event) => {
const message: ChatMessage = JSON.parse(event.data);
// Append new message to existing messages
next(null, (currentMessages: ChatMessage[] = []) => [
...currentMessages,
message
]);
};
socket.onerror = (error) => {
next(new Error("Failed to connect to chat"));
};
return () => socket.close();
}
);
const sendMessage = (message: string) => {
// Would typically use useSWRMutation for sending
// This is just for demonstration
const socket = new WebSocket(`ws://localhost:3001/chat/${roomId}`);
socket.onopen = () => {
socket.send(JSON.stringify({ message, roomId }));
socket.close();
};
};
if (error) return <div>Failed to connect to chat</div>;
return (
<div>
<div>
{messages?.map(msg => (
<div key={msg.id}>
<strong>{msg.user}:</strong> {msg.message}
</div>
))}
</div>
<MessageInput onSend={sendMessage} />
</div>
);
}Live Dashboard:
interface DashboardData {
activeUsers: number;
revenue: number;
orders: number;
timestamp: number;
}
function LiveDashboard() {
const { data: stats, error } = useSWRSubscription(
"/api/dashboard/live",
(key, { next }) => {
const eventSource = new EventSource(key);
eventSource.onmessage = (event) => {
const newStats: DashboardData = JSON.parse(event.data);
next(null, newStats);
};
eventSource.onerror = () => {
next(new Error("Connection to live dashboard failed"));
};
return () => eventSource.close();
}
);
if (error) return <div>Error: {error.message}</div>;
if (!stats) return <div>Connecting...</div>;
return (
<div>
<h1>Live Dashboard</h1>
<div className="stats">
<div>Active Users: {stats.activeUsers}</div>
<div>Revenue: ${stats.revenue}</div>
<div>Orders: {stats.orders}</div>
<div>Last Update: {new Date(stats.timestamp).toLocaleTimeString()}</div>
</div>
</div>
);
}Stock Price Tracker:
interface StockPrice {
symbol: string;
price: number;
change: number;
changePercent: number;
timestamp: number;
}
function StockTracker({ symbols }: { symbols: string[] }) {
const { data: prices, error } = useSWRSubscription(
symbols.length > 0 ? ["stocks", ...symbols] : null,
([, ...symbols], { next }) => {
const ws = new WebSocket("wss://api.example.com/stocks");
ws.onopen = () => {
// Subscribe to symbols
ws.send(JSON.stringify({
action: "subscribe",
symbols: symbols
}));
};
ws.onmessage = (event) => {
const update: StockPrice = JSON.parse(event.data);
// Update prices map
next(null, (currentPrices: Record<string, StockPrice> = {}) => ({
...currentPrices,
[update.symbol]: update
}));
};
ws.onerror = () => {
next(new Error("Stock price feed connection failed"));
};
return () => {
ws.send(JSON.stringify({
action: "unsubscribe",
symbols: symbols
}));
ws.close();
};
}
);
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h2>Stock Prices</h2>
{symbols.map(symbol => {
const price = prices?.[symbol];
return (
<div key={symbol} className={price?.change >= 0 ? "positive" : "negative"}>
<span>{symbol}</span>
<span>${price?.price || "Loading..."}</span>
{price && (
<span>
{price.change >= 0 ? "+" : ""}{price.change}
({price.changePercent}%)
</span>
)}
</div>
);
})}
</div>
);
}Firebase Integration:
function UserPresence({ userId }: { userId: string }) {
const { data: presence } = useSWRSubscription(
userId ? ["firebase", "presence", userId] : null,
([, , userId], { next }) => {
const database = firebase.database();
const userRef = database.ref(`presence/${userId}`);
// Listen for presence changes
const listener = userRef.on("value", (snapshot) => {
const data = snapshot.val();
next(null, {
online: data?.online || false,
lastSeen: data?.lastSeen || null
});
});
// Set user as online
userRef.set({
online: true,
lastSeen: firebase.database.ServerValue.TIMESTAMP
});
// Set offline when disconnected
userRef.onDisconnect().set({
online: false,
lastSeen: firebase.database.ServerValue.TIMESTAMP
});
return () => {
userRef.off("value", listener);
userRef.set({
online: false,
lastSeen: firebase.database.ServerValue.TIMESTAMP
});
};
}
);
return (
<div>
Status: {presence?.online ? "Online" : "Offline"}
{presence?.lastSeen && !presence.online && (
<div>Last seen: {new Date(presence.lastSeen).toLocaleString()}</div>
)}
</div>
);
}Conditional Subscriptions:
function NotificationBell({ userId }: { userId?: string }) {
const [isEnabled, setIsEnabled] = useState(true);
const { data: notifications } = useSWRSubscription(
// Only subscribe if user is logged in and notifications are enabled
userId && isEnabled ? ["notifications", userId] : null,
([, userId], { next }) => {
const eventSource = new EventSource(`/api/notifications/${userId}/stream`);
eventSource.onmessage = (event) => {
const notification = JSON.parse(event.data);
// Accumulate notifications
next(null, (current: Notification[] = []) => [
notification,
...current.slice(0, 9) // Keep only latest 10
]);
};
return () => eventSource.close();
}
);
const unreadCount = notifications?.filter(n => !n.read).length || 0;
return (
<div>
<button onClick={() => setIsEnabled(!isEnabled)}>
🔔 {unreadCount > 0 && <span className="badge">{unreadCount}</span>}
</button>
<div className="notifications">
{notifications?.map(notification => (
<div key={notification.id} className={notification.read ? "read" : "unread"}>
{notification.message}
</div>
))}
</div>
</div>
);
}Install with Tessl CLI
npx tessl i tessl/npm-swr