Recipe: Server-Sent Events (SSE) with Micra.js

This recipe is the canonical answer to “I have an SSE endpoint, how do I wire it to a Micra component?”. It uses only what’s already in Micrastate, onCreate, onDestroy — plus the browser’s native EventSource.

No Micra.stream(...) helper exists. The hard part of SSE — open, parse, handle errors, close — is exactly the kind of thing onCreate/onDestroy already cover. A built-in helper would hide reconnect/auth/named-event concerns without buying much, so we ship a recipe instead.

1. Minimal — one stream, JSON payload

Server route emits data: {"price": 123.45}\n\n lines. Component subscribes on create, closes on destroy.

<div data-component="live-price">
  <strong data-text="price"></strong>
  <small data-text="status"></small>
</div>
Micra.define('live-price', {
  state: {
    price: 0,
    status: 'connecting',
  },

  onCreate() {
    this._es = new EventSource('/api/prices/stream')

    this._es.onopen = () => {
      this.state.status = 'live'
    }

    this._es.onmessage = (e) => {
      const payload = JSON.parse(e.data)
      this.state.price = payload.price
    }

    this._es.onerror = () => {
      // EventSource auto-reconnects with exponential backoff. We just update
      // the UI; we don't close on error or it won't retry.
      this.state.status = 'reconnecting'
    }
  },

  onDestroy() {
    this._es?.close()
  },
})

Micra.start()

Key points:

2. Named events (event: name\ndata: …\n\n)

Production SSE feeds usually split traffic by event name. Use addEventListener per event:

Micra.define('order-feed', {
  state: { orders: [], status: 'connecting' },

  onCreate() {
    const es = new EventSource('/api/orders/stream')
    this._es = es

    es.addEventListener('open', () => {
      this.state.status = 'live'
    })

    es.addEventListener('order:created', (e) => {
      const order = JSON.parse(e.data)
      // Replace, don't mutate — Micra tracks top-level writes only.
      this.state.orders = [order, ...this.state.orders]
    })

    es.addEventListener('order:updated', (e) => {
      const order = JSON.parse(e.data)
      this.state.orders = this.state.orders.map(o =>
        o.id === order.id ? order : o,
      )
    })

    es.addEventListener('error', () => {
      this.state.status = 'reconnecting'
    })
  },

  onDestroy() {
    this._es?.close()
  },
})

3. Stream URL from a server-rendered data-* attribute

SSR-friendly pattern: the server picks the stream URL per page (different tenant, different feed), Micra reads it via this.prop():

<div data-component="notification-feed"
     data-stream="/api/notifications/stream?token=abc123"></div>
Micra.define('notification-feed', {
  state: { items: [], status: 'connecting' },

  onCreate() {
    const url = this.prop('stream')
    if (!url) return  // server didn't render a URL — nothing to do
    this._es = new EventSource(url, { withCredentials: true })
    this._es.onmessage = (e) => {
      this.state.items = [JSON.parse(e.data), ...this.state.items]
    }
    this._es.onopen = () => { this.state.status = 'live' }
    this._es.onerror = () => { this.state.status = 'reconnecting' }
  },

  onDestroy() {
    this._es?.close()
  },
})

4. Manual reconnect with backoff (when native auto-retry isn’t enough)

If your endpoint needs custom auth refresh or a Last-Event-ID header (which the native EventSource does send automatically, but only for plain reconnects), swap in a manual loop. This is more code — only use it when needed.

Micra.define('audit-stream', {
  state: { events: [], status: 'connecting' },

  onCreate() {
    this._closed = false
    this._connect()
  },

  _connect() {
    if (this._closed) return
    const es = new EventSource('/api/audit/stream')
    this._es = es

    es.onopen = () => { this.state.status = 'live' }
    es.onmessage = (e) => {
      this.state.events = [JSON.parse(e.data), ...this.state.events].slice(0, 100)
    }
    es.onerror = () => {
      es.close()
      this.state.status = 'reconnecting'
      this._retry = setTimeout(() => this._connect(), 3000)
    }
  },

  onDestroy() {
    this._closed = true
    clearTimeout(this._retry)
    this._es?.close()
  },
})

5. Multiple streams in one component

state is shared, but you can hold multiple EventSource references:

Micra.define('dashboard', {
  state: { price: 0, volume: 0 },

  onCreate() {
    this._streams = [
      new EventSource('/api/price/stream'),
      new EventSource('/api/volume/stream'),
    ]
    this._streams[0].onmessage = (e) => { this.state.price = JSON.parse(e.data).value }
    this._streams[1].onmessage = (e) => { this.state.volume = JSON.parse(e.data).value }
  },

  onDestroy() {
    this._streams?.forEach(es => es.close())
  },
})

Things to avoid

Backend tips (server-side, not Micra)