@yappr/react
v0.1.0
Published
A thin React binding for Yappr real-time chat. Wrap your tree in `<YapprProvider>`, then read any channel with one hook:
Readme
@yappr/react
A thin React binding for Yappr real-time chat. Wrap your tree in <YapprProvider>, then read any channel with one hook:
const { messages, status, unreadCount, hasOlder, send, loadOlder, markRead } =
useChannel("room1");Requirements
- React 18 or 19 (
react/react-domare peer deps:^18.0.0 || ^19.0.0). - A reachable Yappr server (ws:// or wss://) supplied by your operator.
- Client-rendered — components that call
useChannelor render<YapprProvider>must run in the browser. See SSR.
Install
Once published (the normal case — nothing special):
pnpm add @yappr/react @yappr/coreBefore publish (local tarball testing). @yappr/react's manifest depends on
@yappr/core by version, so installing the react tarball alone makes pnpm look @yappr/core
up on the npm registry and fail with ERR_PNPM_FETCH_404. Drop both tarballs into your project
root, add a pnpm override pinning @yappr/core to the local file, then install:
// package.json
{
"dependencies": {
"@yappr/core": "file:./yappr-core-0.1.0.tgz",
"@yappr/react": "file:./yappr-react-0.1.0.tgz"
},
"pnpm": { "overrides": { "@yappr/core": "file:./yappr-core-0.1.0.tgz" } }
}pnpm install(The override is only needed for local tarballs; once the packages are on npm it is unnecessary.)
Configuration (you must provide these)
createClient needs four values that come from your Yappr server operator —
an AI agent must ASK for these and must not invent them:
| value | what it is | local dev example |
|------------|---------------------------------------------|-----------------------|
| url | the server origin (ws:// or wss://) | ws://localhost:8787 |
| tenantId | your tenant id | t_dev |
| key | your publishable key | pk_test_localdev |
| channel id | which room to join | room1 |
Minimal Next.js (App Router) integration
Copy this into a "use client" component. Substitute the url/tenantId/key your operator gave you and the app's signed-in userId.
// app/chat/chat-client.tsx
"use client";
import { useState } from "react";
import { createClient, YapprProvider, useChannel } from "@yappr/react";
// Created once at module scope → a stable reference (never inline in render).
const client = createClient({
url: "ws://localhost:8787", // ← from your operator
tenantId: "t_dev", // ← from your operator
key: "pk_test_localdev", // ← from your operator
userId: "alice", // the signed-in user; any non-empty string when testing without auth
displayName: "Alice", // optional
});
export function Chat({ channelId = "room1" }: { channelId?: string }) {
return (
<YapprProvider client={client}>
<Room channelId={channelId} />
</YapprProvider>
);
}
function Room({ channelId }: { channelId: string }) {
const { messages, status, send } = useChannel(channelId);
const [draft, setDraft] = useState("");
return (
<div>
<p>status: {status}</p>
<ul>
{messages.map((m) => (
<li key={m.id}>
<b>{m.senderId}:</b> {m.content}
{m.status !== "sent" && <em> ({m.status})</em>}
</li>
))}
</ul>
<form onSubmit={(e) => { e.preventDefault(); if (draft.trim()) { send(draft); setDraft(""); } }}>
<input value={draft} onChange={(e) => setDraft(e.target.value)} />
<button type="submit">Send</button>
</form>
</div>
);
}A Server Component page just renders <Chat />. Everything touching useChannel or <YapprProvider> must live under a "use client" boundary.
@yappr/react and @yappr/core ship compiled JS + types, so no bundler tweaks are needed — you do not need transpilePackages in next.config.
API reference
createClient(config): YapprClient
Re-exported from @yappr/core. Creates a WebSocket client. Call it once — see Gotchas.
| config field | type | required | description |
|----------------|----------|----------|---------------------------------------|
| url | string | yes | WebSocket server origin |
| tenantId | string | yes | your tenant id |
| key | string | yes | your publishable key |
| userId | string | yes | identity of the signed-in user |
| displayName | string | no | display name shown to other users |
<YapprProvider client={client}>
Puts a YapprClient on React context. Stateless — does not open or close the connection.
useChannel(channelId: string): UseChannelResult
Subscribes the component to a channel. Returns:
| field | type | notes |
|---------------|---------------------------------------------------------|-------------------------------------------------------------------|
| messages | Message[] | ascending; includes your own optimistic sends |
| status | "connecting" \| "connected" \| "disconnected" | live connection state |
| unreadCount | number | messages since your last read position |
| hasOlder | boolean | more history available to page in |
| send | (content: string) => void | optimistic; buffered + retried on reconnect if offline. A server rejection yields status: "failed" (see Gotchas) |
| loadOlder | () => Promise<void> | pages older history; resolves when done |
| markRead | (seq: number) => void | advances read position (clears unread badge) |
send, loadOlder, and markRead are stable across renders.
Message shape
type Message =
| (Envelope & { status: "sent" }) // confirmed by server — has a `seq` field
| { status: "sending" | "failed"; // optimistic / not yet confirmed — no `seq`
id: string; senderId: string; content: string; createdAt: number; kind: "user" }seq only exists on confirmed ("sent") messages. Narrow on status before reading it:
const newestSeq = messages.reduce(
(max, m) => (m.status === "sent" && m.seq > max ? m.seq : max),
0,
);
markRead(newestSeq);Gotchas
Stable client — create once. useChannel never closes channels; the client owns connection lifetime. If you write <YapprProvider client={createClient(...)}> inline in render, a new socket is opened every render and the old one is orphaned. Create the client at module scope (or in a useEffect with a cleanup that calls client.close()).
"use client" is required. In Next.js App Router, any component that calls useChannel or renders <YapprProvider> must be inside a "use client" boundary. Server Components cannot use these.
send is optimistic. It returns immediately and the message appears in messages with status: "sending". It flips to status: "sent" when the server confirms, or status: "failed" if rejected. Rendering a retry UI for "failed" messages is app-level responsibility — @yappr/react surfaces the status but does not render UI for it.
Failed sends are not retried automatically. A "failed" message stays in the list. If you want retry, call send(m.content) again when the user triggers it.
Key safety
The key value (pk_test_localdev in local dev) is a publishable key. Whether it is safe to ship in a browser bundle is a production and operator decision. The local pk_test_localdev is for local development only and should never be used in production.
SSR
useChannel provides a stable getServerSnapshot so server rendering or prerendering does not crash or hydration-mismatch. On the server (and the first hydration render) the snapshot is { status: "connecting", messages: [], unreadCount: 0, hasOlder: false }; the WebSocket is opened only in the browser, and no messages are fetched on the server. In Next.js App Router, mark any component using this package with "use client".
For AI agents
- Ask the human for
url,tenantId, andkey(see Configuration). Do not invent them. - Install the package(s) (see Install).
- Create a
"use client"component using the snippet above; substitute the providedurl/tenantId/keyand the app's signed-inuserId. - Render it from a page. In Next.js App Router, the component (or its wrapper) must be
"use client". - Verify: the status shows
connectedand a sent message appears and turnssent.
Contributor note (monorepo developers)
This package is part of the yappr monorepo. For local development and a runnable demo, see apps/web. From the repo root:
pnpm --filter @yappr/server-app dev:setup # once: migrate + seed tenant t_dev / key pk_test_localdev
pnpm --filter @yappr/server-app dev # Yappr server on :8787
pnpm --filter @yappr/web dev # demo app (open the printed URL)The demo's ChatPane/ChannelView are a working reference implementation for the patterns in this README.
