Voice UI Kit

usePipecatEventStream

Hook to subscribe to Pipecat RTVI events with throttled updates and optional grouping.

Overview

usePipecatEventStream subscribes to Pipecat RTVI events and exposes a throttled snapshot of recent events. It minimizes re-renders during high-frequency event bursts and can optionally group consecutive events by a key.

Usage

Basic

import { usePipecatEventStream } from "@pipecat-ai/voice-ui-kit";

export function RecentEvents() {
  const { events, clear } = usePipecatEventStream({ maxEvents: 100 });
  return (
    <div>
      <button onClick={clear}>Clear</button>
      <ul>
        {events.map((e) => (
          <li key={e.id}>
            <strong>{e.type}</strong>
            <span> – {e.timestamp.toLocaleTimeString()}</span>
          </li>
        ))}
      </ul>
    </div>
  );
}

Filter and Throttle

const { events } = usePipecatEventStream({
  includeEvents: ["serverMessage", "serverResponse"],
  throttleMs: 100, // batch UI updates every 100ms
});

Group Consecutive

const { groups } = usePipecatEventStream({
  groupConsecutive: true,
  groupKey: (e) => e.type,
});

Full example

import React, { useEffect, useRef, useState } from "react";
import {
  usePipecatEventStream,
  type PipecatEventGroup,
} from "@pipecat-ai/voice-ui-kit";

export function EventStreamExample() {
  const { events, groups } = usePipecatEventStream({
    maxEvents: 200,
    groupConsecutive: true,
  });
  const [expanded, setExpanded] = useState<Set<string>>(new Set());
  const endRef = useRef<HTMLDivElement>(null);

  // Auto-scroll on new events
  useEffect(() => {
    endRef.current?.scrollIntoView({ behavior: "smooth" });
  }, [events]);

  const toggleGroup = (id: string) => {
    setExpanded((prev) => {
      const next = new Set(prev);
      next.has(id) ? next.delete(id) : next.add(id);
      return next;
    });
  };

  return (
    <div style={{ height: 300, overflow: "auto", fontFamily: "monospace", fontSize: 12 }}>
      {groups.map((group: PipecatEventGroup) => {
        const isExpanded = expanded.has(group.id);
        const hasMultiple = group.events.length > 1;

        if (!hasMultiple || isExpanded) {
          return group.events.map((e, idx) => (
            <div key={e.id}>
              {hasMultiple && idx === 0 ? (
                <button onClick={() => toggleGroup(group.id)}>[−]</button>
              ) : (
                <span>•</span>
              )}
              <span> [{e.timestamp.toLocaleTimeString()}] </span>
              <strong>{e.type}</strong> {e.data ? JSON.stringify(e.data) : "null"}
            </div>
          ));
        }

        return (
          <div key={group.id}>
            <button onClick={() => toggleGroup(group.id)}>[+]</button>
            <span> [{group.events[0].timestamp.toLocaleTimeString()}] </span>
            <strong>{group.type}</strong> ({group.events.length} events)
          </div>
        );
      })}
      <div ref={endRef} />
    </div>
  );
}

API

Options

PropTypeDefault
maxEvents?
number
500
ignoreEvents?
string[]
[RTVIEvent.LocalAudioLevel]
includeEvents?
string[]
undefined
paused?
boolean
false
mapEventData?
(data: unknown, eventType: string) => unknown
undefined
throttleMs?
number
0
onEvent?
(event: PipecatEventLog) => void
undefined
groupConsecutive?
boolean
false
groupKey?
(event: PipecatEventLog) => string
(e) => e.type

Return

PropTypeDefault
events
ReadonlyArray<PipecatEventLog>
-
groups
ReadonlyArray<PipecatEventGroup>
-
clear
() => void
-