CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-swr

React Hooks library for remote data fetching with stale-while-revalidate caching strategy

Pending
Overview
Eval results
Files

subscriptions.mddocs/

Real-time Subscriptions

The useSWRSubscription hook enables real-time data updates through WebSockets, Server-Sent Events, or other subscription mechanisms.

Capabilities

useSWRSubscription Hook

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);
  }
);

SWR Subscription Response

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;
}

Subscription Function

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();
};

Advanced Subscription Patterns

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

docs

cache-management.md

core-data-fetching.md

global-configuration.md

immutable-data.md

index.md

infinite-loading.md

mutations.md

subscriptions.md

tile.json