npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

reactive-swr

v0.1.0

Published

Meteor-style reactivity for React using SWR and Server-Sent Events

Readme

reactiveSWR

A lightweight library that brings Meteor-style reactivity to modern React applications using SWR and Server-Sent Events (SSE).

The Problem

Building real-time UIs typically requires:

  • Manual SSE/WebSocket listeners scattered across components
  • Ad-hoc cache invalidation logic
  • Components tightly coupled to real-time transport details
  • Easy-to-miss cache updates when data changes

The Solution

reactiveSWR provides a declarative bridge between SSE events and SWR's cache. Define a shared schema once, and your components just use normal useSWR hooks -- they automatically receive real-time updates without knowing about SSE.

import { defineSchema } from 'reactive-swr'

// Define your schema once -- shared by server and client
const schema = defineSchema({
  'order:updated': {
    key: (p: { id: string; status: string }) => `/api/orders/${p.id}`,
    update: 'set',
  },
  'comment:added': {
    key: (p: { postId: string; comment: string }) => `/api/posts/${p.postId}/comments`,
    update: (current: string[] | undefined, p) => [...(current ?? []), p.comment],
  },
})

// Components just use useSWR - updates happen automatically
function OrderStatus({ orderId }) {
  const { data } = useSWR(`/api/orders/${orderId}`)
  return <div>Status: {data?.status}</div>  // Real-time!
}

You can also define event mappings manually without a schema -- see Manual Events Mapping below.

Installation

npm install reactive-swr swr

Quick Start

1. Define a schema (shared between server and client)

// schema.ts
import { defineSchema } from 'reactive-swr'

export const schema = defineSchema({
  'user:updated': {
    key: (p: { id: string }) => `/api/users/${p.id}`,
    update: 'set',
  },
  'order:placed': {
    key: '/api/orders',
    update: 'refetch',
  },
})

2. Server: create an SSE channel

// server.ts
import { createChannel } from 'reactive-swr/server'
import { schema } from './schema'

const channel = createChannel(schema)

// Web standard (Cloudflare Workers, Deno, Bun)
export function GET(request: Request) {
  return channel.connect(request)
}

// Node.js HTTP / Express / Fastify
app.get('/api/events', (req, res) => channel.connect(req, res))

// Broadcast type-safe events
channel.emit('user:updated', { id: '42', name: 'Alice' })

3. Client: wire up SSEProvider with the schema

import { SWRConfig } from 'swr'
import { SSEProvider } from 'reactive-swr'
import { schema } from './schema'

function App() {
  return (
    <SWRConfig value={{ fetcher: (url) => fetch(url).then(r => r.json()) }}>
      <SSEProvider config={{ url: '/api/events', schema }}>
        <YourApp />
      </SSEProvider>
    </SWRConfig>
  )
}

Components use standard useSWR hooks and receive real-time updates automatically.

Features

Schema-Driven SSE

The recommended approach is to define a shared schema that drives both server-side event emission and client-side cache updates. This eliminates type drift between server and client.

defineSchema()

defineSchema() creates a frozen, type-safe schema object. Event names are preserved as string literals for full TypeScript autocomplete on both sides.

import { defineSchema } from 'reactive-swr'

const schema = defineSchema({
  'user:updated': {
    key: (p: { id: string; name: string }) => `/api/users/${p.id}`,
    update: 'set',
  },
  'stats:refreshed': {
    key: ['/api/stats', '/api/dashboard'],
    update: 'refetch',
  },
  'comment:added': {
    key: (p: { postId: string; comment: Comment }) => `/api/posts/${p.postId}/comments`,
    update: (current: Comment[] | undefined, p) => [...(current ?? []), p.comment],
    filter: (p) => !p.comment.deleted,
    transform: (p) => ({ ...p, comment: { ...p.comment, isNew: true } }),
  },
})

Each event definition supports:

| Property | Type | Description | |----------|------|-------------| | key | string \| string[] \| (payload) => string \| string[] | SWR cache key(s) to update | | update | 'set' \| 'refetch' \| (current, payload) => newValue | Update strategy (default: 'set') | | filter | (payload) => boolean | Optional client-side filter | | transform | (payload) => payload | Optional client-side transform |

createChannel() (Server)

createChannel() provides a complete server-side SSE endpoint. It handles wire formatting, heartbeats, connection tracking, and cleanup. Import it from reactive-swr/server.

import { createChannel } from 'reactive-swr/server'

const channel = createChannel(schema, {
  heartbeatInterval: 30000, // default: 30s
})

Dual runtime support -- works with both Web standard APIs (Cloudflare Workers, Deno, Bun) and Node.js (Express, Fastify, raw http):

// Web standard: returns a streaming Response
export function GET(request: Request): Response {
  return channel.connect(request)
}

// Node.js: writes to ServerResponse
app.get('/events', (req, res) => {
  channel.connect(req, res)
})

Broadcast events to all connected clients:

// Type-safe: eventType and payload are checked against the schema
channel.emit('user:updated', { id: '42', name: 'Alice' })

Scoped emitters for request-response patterns (e.g., streaming query results):

// Node.js
app.post('/api/query', (req, res) => {
  const emitter = channel.respond(req, res)
  emitter.emit('result', { rows: queryResults })
  emitter.close()
})

// Web standard (Fetch API / edge runtimes)
export async function POST(request: Request) {
  const { response, emitter } = channel.respond(request)
  emitter.emit('result', { rows: await queryDB() })
  emitter.close()
  return response
}

Shutdown all connections:

channel.close() // Closes all connections, stops heartbeats

SSEProvider schema Prop

Pass a schema to SSEProvider instead of manually writing events mappings:

<SSEProvider config={{ url: '/api/events', schema }}>
  <App />
</SSEProvider>

The events mapping is automatically derived from the schema's key, update, filter, and transform definitions. schema and events are mutually exclusive -- providing both is a TypeScript error. The parseEvent callback remains configurable alongside schema.

Manual Events Mapping

If you prefer not to use a schema, you can define event mappings manually. This is the original API and remains fully supported.

const config: SSEConfig = {
  url: '/api/events',
  events: {
    'order:updated': {
      key: (p) => `/api/orders/${p.id}`,
      update: 'set',
    },
  },
}

<SSEProvider config={config}>
  <App />
</SSEProvider>

Update Strategies

Control how SSE events update your cached data:

  • 'set' - Replace cache with the event payload (no network request)
  • 'refetch' - Trigger SWR revalidation (ignores payload)
  • Custom function - Merge payload with current data: (current, payload) => newValue
const config: SSEConfig = {
  url: '/api/events',
  events: {
    // Replace entire cache entry
    'user:updated': {
      key: (p) => `/api/users/${p.id}`,
      update: 'set',
    },
    // Trigger refetch from server
    'cache:invalidate': {
      key: (p) => p.keys,
      update: 'refetch',
    },
    // Custom merge logic
    'comment:added': {
      key: (p) => `/api/posts/${p.postId}/comments`,
      update: (current, p) => [...(current ?? []), p.comment],
    },
  },
}

Dynamic Keys

Keys can be static strings, arrays, or functions:

events: {
  // Static key
  'stats:updated': {
    key: '/api/stats',
    update: 'set',
  },
  // Multiple keys
  'user:updated': {
    key: ['/api/users', '/api/user-count'],
    update: 'refetch',
  },
  // Dynamic key from payload
  'order:updated': {
    key: (p) => `/api/orders/${p.id}`,
    update: 'set',
  },
}

Filter and Transform

Pre-process events before they update the cache:

events: {
  'order:updated': {
    key: (p) => `/api/orders/${p.id}`,
    // Only process orders for current user
    filter: (p) => p.userId === currentUserId,
    // Extract just the order data
    transform: (p) => p.order,
    update: 'set',
  },
}

POST SSE and Custom Transports

By default, reactiveSWR uses the browser's EventSource API, which only supports GET requests. The transport abstraction lets you connect to SSE endpoints that require POST requests, custom headers, or entirely custom connection logic.

POST with JSON body

import { useSSEStream } from 'reactive-swr'

function AIChat({ question }: { question: string }) {
  const { data, error } = useSSEStream<string>('/api/chat', {
    method: 'POST',
    body: { question, model: 'gpt-4' },
  })

  return <div>{data}</div>
}

Plain objects passed as body are automatically JSON-serialized with Content-Type: application/json. If you provide a body without a method, it defaults to POST.

Custom headers (authenticated SSE)

const { data } = useSSEStream('/api/events', {
  headers: { Authorization: `Bearer ${token}` },
})

Providing headers (or method or body) automatically switches from EventSource to the fetch-based transport.

Custom transport factory

For full control, provide a transport factory that returns an SSETransport-compatible object:

import type { SSETransport } from 'reactive-swr'

const { data } = useSSEStream('/api/events', {
  transport: (url) => createMyCustomTransport(url),
})

SSEProvider with transport options

The same transport options are available in SSEConfig:

const config: SSEConfig = {
  url: '/api/events',
  method: 'POST',
  body: { subscribe: ['orders', 'users'] },
  headers: { Authorization: `Bearer ${token}` },
  events: {
    'order:updated': {
      key: (p) => `/api/orders/${p.id}`,
      update: 'set',
    },
  },
}

// Or with a custom transport factory:
const config: SSEConfig = {
  url: '/api/events',
  transport: (url) => createMyCustomTransport(url),
  events: { /* ... */ },
}

SSE Parser

For advanced users building custom transports, the SSE wire format parser is available as a standalone export:

import { createSSEParser } from 'reactive-swr'

const parser = createSSEParser({
  onEvent(event) {
    console.log(event.event, event.data, event.id)
  },
  onRetry(ms) {
    console.log('Server requested retry interval:', ms)
  },
})

// Feed raw SSE text (handles chunked input)
parser.feed('data: {"hello":"world"}\n\n')
parser.feed('event: update\ndata: {"id":1}\n\n')

Reconnection

Automatic reconnection with exponential backoff:

const config: SSEConfig = {
  url: '/api/events',
  events: { /* ... */ },
  reconnect: {
    enabled: true,           // default: true
    initialDelay: 1000,      // default: 1000ms
    maxDelay: 30000,         // default: 30000ms
    backoffMultiplier: 2,    // default: 2
    maxAttempts: Infinity,   // default: Infinity
  },
}

The connection also auto-reconnects when a hidden browser tab becomes visible.

Connection Callbacks

React to connection lifecycle events:

const config: SSEConfig = {
  url: '/api/events',
  events: { /* ... */ },
  onConnect: () => {
    console.log('Connected to SSE')
  },
  onDisconnect: () => {
    toast.warning('Connection lost. Reconnecting...')
  },
  onError: (error) => {
    captureException(error)
  },
  onEventError: (event, error) => {
    console.error(`Failed to process ${event.type}:`, error)
  },
}

Debug Mode

Enable console logging for SSE events:

const config: SSEConfig = {
  url: '/api/events',
  events: { /* ... */ },
  debug: true,  // Logs events and unhandled event types
}

Hooks

useSSEStatus

Access connection status from any component:

import { useSSEStatus } from 'reactive-swr'

function ConnectionIndicator() {
  const { connected, connecting, error, reconnectAttempt } = useSSEStatus()

  if (error) return <span>Error: {error.message}</span>
  if (connecting) return <span>Connecting... (attempt {reconnectAttempt})</span>
  if (connected) return <span>Connected</span>
  return <span>Disconnected</span>
}

useSSEEvent

Subscribe to raw SSE events outside the declarative config:

import { useSSEEvent } from 'reactive-swr'

function NotificationListener() {
  useSSEEvent<{ message: string }>('notification', (payload) => {
    toast.info(payload.message)
  })

  return null
}

useSSEStream

Create an independent SSE connection (bypasses the provider):

import { useSSEStream } from 'reactive-swr'

function LivePrice({ symbol }: { symbol: string }) {
  const { data, error } = useSSEStream<number>(
    `/api/prices/${symbol}`,
    { transform: (raw) => (raw as { price: number }).price }
  )

  if (error) return <span>--</span>
  return <span>${data?.toFixed(2)}</span>
}

Options

| Option | Type | Description | |--------|------|-------------| | transform | (data: unknown) => T | Transform incoming data before storing | | method | string | HTTP method (defaults to POST when body is provided) | | body | BodyInit \| Record<string, unknown> | Request body (triggers fetch-based transport) | | headers | Record<string, string> | Additional request headers (triggers fetch-based transport) | | transport | (url: string) => SSETransport | Custom transport factory (takes precedence over all other options) |

Testing

The library provides mockSSE for testing components with SSE:

import { mockSSE } from 'reactive-swr/testing'

test('updates order when SSE event received', async () => {
  const mock = mockSSE('/api/events')

  render(
    <SSEProvider config={sseConfig}>
      <OrderStatus orderId="123" />
    </SSEProvider>
  )

  // Initial state
  expect(screen.getByText('Status: pending')).toBeInTheDocument()

  // Simulate SSE event
  mock.sendEvent({
    type: 'order:updated',
    payload: { id: '123', status: 'shipped' },
  })

  // Verify update
  await waitFor(() => {
    expect(screen.getByText('Status: shipped')).toBeInTheDocument()
  })

  // Clean up
  mockSSE.restore()
})

mockSSE API

const mock = mockSSE(url: string)

mock.sendEvent({ type: string, payload: unknown })  // Send a typed event
mock.sendSSE(data: unknown)                          // Send data as SSE wire format — data: <json>\n\n (convenience for createSSEParser tests)
mock.sendRaw(text: string)                           // Send raw SSE wire format
mock.close()                                         // Simulate connection close
mock.getConnection()                                 // Get the mock EventSource

mockSSE.restore()                                    // Restore real EventSource and fetch

sendSSE(data) is a convenience wrapper that formats data as data: <json>\n\n and sends it via sendRaw(). It simplifies tests for consumers using createSSEParser who work with raw SSE wire format.

mockSSE automatically intercepts both EventSource and fetch for registered URLs, so your tests work regardless of which transport the component uses internally.

Documentation

Inspiration

This library is inspired by Meteor's Minimongo and DDP protocol, which pioneered the pattern of real-time database synchronization to the client. reactiveSWR brings that developer experience to the modern React ecosystem using SSE and SWR.

Contributing

Contributions are welcome! This project uses Bun as its runtime and package manager.

Setup

git clone https://github.com/queso/reactiveSWR.git
cd reactiveSWR
bun install

Development commands

bun run dev        # Watch mode
bun test           # Run unit tests
bun run lint       # Check for lint issues
bun run lint:fix   # Auto-fix lint issues
bun run typecheck  # Type-check without emitting
bun run build      # Build dist/ artifacts

Workflow

  1. Fork the repo and create a feature branch
  2. Make your changes
  3. Ensure bun run lint, bun run typecheck, and bun test all pass
  4. Open a pull request against main

License

MIT