Client Library
Framework Bindings
Reactive useLiveChat adapters for React, Vue and Svelte, all over one framework-agnostic core.
One core, three adapters
The client ships a framework-agnostic store (LiveChatStore, exported from
the root entry) that owns every behaviour — dedupe, buffer capping, counting,
the status machine. Each framework binding is a thin layer that binds that
store to component lifetime and reactivity, so all three behave identically.
Import the adapter from its subpath. React itself never enters your bundle unless you import the React entry:
import { useLiveChat } from '@gettersethya/yt-livechat-client/react'
import { useLiveChat } from '@gettersethya/yt-livechat-client/vue'
import { useLiveChat } from '@gettersethya/yt-livechat-client/svelte'| Subpath | Peer dependency | Binding |
|---|---|---|
/react |
react ^18 || ^19 |
useSyncExternalStore |
/vue |
vue ^3.5 |
shallowRef + scope disposal |
/svelte |
svelte ^5 |
svelte/store readable |
All peers are optional — install only the one you use:
npm install @gettersethya/yt-livechat-client reactpnpm add @gettersethya/yt-livechat-client reactyarn add @gettersethya/yt-livechat-client reactbun add @gettersethya/yt-livechat-client reactThe state shape
Every adapter returns the same LiveChatState snapshot:
| Field | Type | Description |
|---|---|---|
status |
'connecting' | 'live' | 'ended' | 'error' |
Stream state machine |
messages |
readonly MessageSchema[] |
Newest-last transcript, capped by maxMessages |
totalMessages |
number |
Every unique message seen this session, including trimmed ones |
detail |
string | null |
End reason, or "CODE: message" after an error |
error |
ApiHttpError | null |
The last error object, if any |
Duplicate messages are filtered internally (YouTube can resend across polls), so neither the transcript nor the count double-counts.
Options
useLiveChat({ baseUrl, videoUrl }, {
maxMessages: 5000,
dedupeWindow: 10000,
})| Option | Default | Description |
|---|---|---|
maxMessages |
5000 |
Transcript cap; oldest messages drop off the front |
dedupeWindow |
10000 |
How many recent ids to remember when filtering resends |
The core client's options — messageSpacingMs,
fetchFn — pass straight through as well.
React
import { useLiveChat } from '@gettersethya/yt-livechat-client/react'
function Chat({ baseUrl, videoUrl }: Props) {
const { messages, status, detail, totalMessages } = useLiveChat({
baseUrl,
videoUrl,
})
return (
<>
<p>{status} — {totalMessages} messages</p>
<ul>
{messages.map((message) => (
<li key={message.id}>{message.author}: {message.message}</li>
))}
</ul>
</>
)
}The connection lives for as long as the component does: started on mount,
stopped on unmount, safe under StrictMode's double effects. To follow another
video, give the component a new key rather than mutating props mid-flight —
the hook binds to one stream per instance.
useVideoId(url) is also exported; it memoizes extractVideoId and returns
null for malformed input.
Vue
<script setup lang="ts">
import { useLiveChat } from '@gettersethya/yt-livechat-client/vue'
const props = defineProps<{ baseUrl: string; videoUrl: string }>()
const chat = useLiveChat(props)
console.log(chat.value.status)
</script>
<template>
<p>{{ chat.totalMessages }} messages — {{ chat.status }}</p>
<ul>
<li v-for="message in chat.messages" :key="message.id">
{{ message.author }}: {{ message.message }}
</li>
</ul>
</template>The returned value is a readonly ref; templates unwrap it automatically. Work
is bound to the component scope — starting waits for onMounted, so server-side
rendering stays network-silent — and everything stops when the scope disposes.
Vue's version accepts more than the others where it makes sense:
useVideoId(videoUrl) takes a string, ref or getter and returns a computed.
For the live-chat input itself, treat the stream as keyed to the component and
re-mount with a changed :key when the video changes.
Svelte
The Svelte binding is a plain svelte/store
readable, so it works with $store auto-subscription in components and with
runes alike:
<script lang="ts">
import { useLiveChat } from '@gettersethya/yt-livechat-client/svelte'
let { baseUrl, videoUrl }: Props = $props()
const chat = useLiveChat({ baseUrl, videoUrl })
</script>
<p>{$chat.totalMessages} messages — {$chat.status}</p>
<ul>
{#each $chat.messages as message (message.id)}
<li>{message.author}: {message.message}</li>
{/each}
</ul>The store starts when first subscribed (i.e. when the template reads $chat)
and stops on the final unsubscribe, which is component teardown in practice.
Wrap the consumer in an {#key videoUrl} block to move to another video.
Because it is a real store you can derive from it: $derived($chat.messages.length)
and friends work unchanged.
useVideoId(url) mirrors the other adapters as a pure helper around
extractVideoId.
Building your own binding
If none of these fit — Solid, Angular signals, a terminal UI — compose the exported core directly. It exposes exactly four members:
import { LiveChatStore } from '@gettersethya/yt-livechat-client'
const store = new LiveChatStore({ baseUrl, videoUrl })
store.subscribe((state) => render(state)) // returns an unsubscribe fn
void store.start() // resolves when the stream ends
store.stop() // cancels; safe before/during startYour framework layer only has to translate subscribe/getState into its
reactivity primitive and call start()/stop() on mount/unmount. The three
adapters above are each ~15 lines doing precisely that — read their source as
reference implementations.