{"_id":"@akshilmy/eventloom-react","name":"@akshilmy/eventloom-react","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@akshilmy/eventloom-react","version":"0.1.0","description":"React adapter for eventloom: useEventStream hook, StreamView component, and a type-safe component registry.","type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"sideEffects":false,"scripts":{"build":"tsup src/index.ts --format esm,cjs --dts --sourcemap --clean","test":"vitest run","typecheck":"tsc --noEmit"},"keywords":["react","sse","server-sent-events","streaming","events","real-time"],"author":{"name":"Akshil"},"license":"MIT","homepage":"https://github.com/AKSHILMY/eventloom/tree/main/typescript/packages/react","repository":{"type":"git","url":"git+https://github.com/AKSHILMY/eventloom.git","directory":"typescript/packages/react"},"publishConfig":{"access":"public"},"peerDependencies":{"react":">=18.0.0"},"dependencies":{"@akshilmy/eventloom-core":"^0.1.0"},"devDependencies":{"@testing-library/jest-dom":"^6.6.3","@testing-library/react":"^16.0.1","@types/react":"^18.3.12","@types/react-dom":"^18.3.1","jsdom":"^25.0.1","react":"^18.3.1","react-dom":"^18.3.1","typescript":"^5.6.3","tsup":"^8.3.5","vitest":"^2.1.8"},"gitHead":"4a40739d0742ff8ec8ce22e63b69eab205db9186","_id":"@akshilmy/eventloom-react@0.1.0","bugs":{"url":"https://github.com/AKSHILMY/eventloom/issues"},"_nodeVersion":"25.9.0","_npmVersion":"11.12.1","dist":{"integrity":"sha512-exeNpcem/qT4BvXcAVjkyBgKCQzXPe2tlUlrhGuX4Ew6nIRP7pr83O3zwtW5cjq/obQiGOMarM/J0EX0PVXiBA==","shasum":"970eef4c30dc8c14850f52ef92bfebdd661752ab","tarball":"https://registry.npmjs.org/@akshilmy/eventloom-react/-/eventloom-react-0.1.0.tgz","fileCount":9,"unpackedSize":49294,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIAYeLYy7DTfjxN1D1Nc/xna8Mcxts7ESk93IsSuKw24lAiA2fUpX46Up4r4i7XFKs47lmj8rFdtBptGLSjpzf7oDeg=="}]},"_npmUser":{"name":"akshilmy","email":"akshilmy.19@gmail.com"},"directories":{},"maintainers":[{"name":"akshilmy","email":"akshilmy.19@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/eventloom-react_0.1.0_1787071081886_0.3275607005688439"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-18T16:38:01.553Z","0.1.0":"2026-08-18T16:38:02.031Z","modified":"2026-08-18T16:38:02.331Z"},"maintainers":[{"name":"akshilmy","email":"akshilmy.19@gmail.com"}],"description":"React adapter for eventloom: useEventStream hook, StreamView component, and a type-safe component registry.","homepage":"https://github.com/AKSHILMY/eventloom/tree/main/typescript/packages/react","keywords":["react","sse","server-sent-events","streaming","events","real-time"],"repository":{"type":"git","url":"git+https://github.com/AKSHILMY/eventloom.git","directory":"typescript/packages/react"},"author":{"name":"Akshil"},"bugs":{"url":"https://github.com/AKSHILMY/eventloom/issues"},"license":"MIT","readme":"# @akshilmy/eventloom-react\n\nReact bindings for [eventloom](https://github.com/akshilmy/eventloom): a `useEventStream` hook, a\n`<StreamView>` component, and a type-safe registry that maps backend event types to your\ncomponents. Built on [`@akshilmy/eventloom-core`](https://www.npmjs.com/package/@akshilmy/eventloom-core)\n(SSE connection + merge logic); pairs with the Python [`eventloom`](https://pypi.org/project/eventloom/)\n+ [`eventloom[fastapi]`](https://pypi.org/project/eventloom/) packages on the backend, but works\nagainst any backend that emits the same [wire protocol](#wire-protocol) over SSE.\n\n- No LLM/agent required, fully deterministic: the backend decides what events mean, you decide\n  what component renders each one.\n- Type safety: registering the wrong component for an event type is a compile error.\n- Discrete and partial events are both first-class — merge strategy is chosen per event type.\n\n## Install\n\n```bash\nnpm install @akshilmy/eventloom-react\n```\n\n`@akshilmy/eventloom-core` is a dependency and installs automatically. Requires React 18+.\n\n## Quickstart\n\n**1. Define a component per event type.** Each one receives `{ data, done, id }`:\n\n```tsx\n// components.tsx\ninterface ChartData {\n  labels: string[];\n  values: number[];\n}\n\nfunction ChartWidget({ data, done }: { data: ChartData; done: boolean; id: string }) {\n  return <BarChart labels={data.labels} values={data.values} loading={!done} />;\n}\n\ninterface UserProfile {\n  name?: string;\n  bio?: string;\n}\n\nfunction UserCard({ data }: { data: UserProfile; done: boolean; id: string }) {\n  return (\n    <div>\n      <h3>{data.name ?? \"Loading…\"}</h3>\n      <p>{data.bio ?? \"\"}</p>\n    </div>\n  );\n}\n\n// \"append\" strategy accumulates into an array — see the note under EventStore\n// in @akshilmy/eventloom-core's README. Render every item, not just the latest.\nfunction LogViewer({ data }: { data: Array<{ text: string }>; done: boolean; id: string }) {\n  return (\n    <pre>\n      {data.map((line, i) => (\n        <div key={i}>{line.text}</div>\n      ))}\n    </pre>\n  );\n}\n```\n\n**2. Build a registry mapping event type -> component:**\n\n```tsx\nimport { createRegistry } from \"@akshilmy/eventloom-react\";\n\nconst registry = createRegistry()\n  .register(\"chart.data\", { renderer: ChartWidget })\n  .register(\"user.partial\", { renderer: UserCard, strategy: \"merge\" })\n  .register(\"log.line\", { renderer: LogViewer, strategy: \"append\" });\n```\n\n**3. Point `<StreamView>` at your backend endpoint:**\n\n```tsx\nimport { StreamView } from \"@akshilmy/eventloom-react\";\n\nfunction Dashboard() {\n  return <StreamView endpoint=\"/stream/dashboard\" registry={registry} />;\n}\n```\n\nThat's the whole integration. Every envelope your backend emits on `/stream/dashboard` gets\ncombined per its merge strategy and routed to the matching component automatically.\n\n## Type safety\n\n```tsx\nconst BadWidget: React.FC<{ data: { wrongShape: true }; done: boolean; id: string }> = () => null;\n\ncreateRegistry().register(\"chart.data\", { renderer: BadWidget });\n//                                                    ~~~~~~~~~\n// Type error if you also pin the expected payload type:\ncreateRegistry().register<\"chart.data\", ChartData>(\"chart.data\", { renderer: BadWidget });\n// ❌ Type 'FC<{ data: { wrongShape: true }; ... }>' is not assignable to\n//    type 'ComponentType<EventComponentProps<ChartData>>'\n```\n\nAuto-inference (no explicit generics needed) works for the common case — passing a component\nwith a concrete `data` prop type just infers the payload type from it. Pin `<Type, Payload>`\nexplicitly when you want the compiler to check a renderer against a payload type you maintain by\nhand (matching your backend's Pydantic model) or, later, generate via the optional codegen bridge\ndescribed in the project plan — not required for v1, but the registry API is designed to support it\nwithout changes once it exists.\n\n## `useEventStream` — for custom layouts\n\nUse this instead of `<StreamView>` when you need custom layout, filtering, or ordering:\n\n```tsx\nimport { useEventStream } from \"@akshilmy/eventloom-react\";\n\nfunction Dashboard() {\n  const { events, status, error } = useEventStream(\"/stream/dashboard\", { registry });\n\n  if (status === \"error\") return <ErrorBanner error={error} />;\n\n  return (\n    <div className=\"grid grid-cols-2\">\n      {events.map((event) => {\n        const config = registry.get(event.type);\n        if (!config) return null;\n        const Component = config.renderer;\n        return <Component key={event.id} id={event.id} data={event.data} done={event.done} />;\n      })}\n    </div>\n  );\n}\n```\n\n`useEventStream(endpoint, options)` returns:\n\n- `events: EventSnapshot[]` — every `id`'s current combined state.\n- `status: \"connecting\" | \"open\" | \"closed\" | \"error\"`. `\"closed\"` means the backend ended the\n  stream normally and it won't be retried (see [Reconnection](#reconnection)) — a finite stream\n  (emit some events, mark `done`, close) settles here, not stuck on `\"open\"` forever.\n- `error: unknown` — the last connection error, if `status === \"error\"`.\n\n`options` (all optional): `registry` (for per-type strategy overrides), `fetchOptions` (headers,\ncredentials — see [auth](#auth--credentials)), `reconnect`, `reconnectOnComplete`,\n`initialReconnectDelayMs`, `maxReconnectDelayMs`, `onError`, `onComplete`.\n\nOnly `endpoint` re-triggers a reconnect; other options are read fresh per event but don't tear\ndown and rebuild the connection on every render — change `endpoint` (or unmount/remount) to force\na fresh connection with new options.\n\n## Error handling\n\nIf the backend's stream fails mid-way (see the Python package's `to_sse_response` docs), it sends\na built-in `__stream_error__`-typed envelope as the last event. Apps never register that type\nthemselves, so it always falls through to `<StreamView>`'s `fallback`:\n\n```tsx\nimport { STREAM_ERROR_TYPE, type StreamErrorData } from \"@akshilmy/eventloom-react\";\n\nfunction Fallback({ data, id }: { data: unknown; done: boolean; id: string }) {\n  if (id === STREAM_ERROR_TYPE) {\n    const error = data as StreamErrorData;\n    return <ErrorBanner message={error.message} code={error.code} />;\n  }\n  return <UnknownEventCard />; // any other unregistered type\n}\n\n<StreamView endpoint=\"/stream/dashboard\" registry={registry} fallback={Fallback} />;\n```\n\nA connection-level failure (can't reach the server at all, non-2xx response) surfaces instead via\n`useEventStream`'s `status`/`error` (or `<StreamView>`'s `onError` prop, forwarded straight\nthrough) — that's a different failure mode than a mid-stream `__stream_error__` envelope, since the\nlatter requires the HTTP response to have started successfully first.\n\n## Auth / credentials\n\n`EventSource` (the browser's native SSE client) can't set custom headers — this package uses\n`fetch` internally instead specifically so you can:\n\n```tsx\nuseEventStream(\"/stream/dashboard\", {\n  fetchOptions: {\n    headers: { Authorization: `Bearer ${token}` },\n    credentials: \"include\", // send cookies\n  },\n});\n```\n\n## Reconnection\n\nOn a dropped or failed connection (network error, non-2xx response), `useEventStream`/`<StreamView>`\nreconnect automatically with exponential backoff (1s → 2s → 4s → ... capped at 30s by default).\nDisable with `reconnect: false`, or tune `initialReconnectDelayMs`/`maxReconnectDelayMs`.\n\n**A clean close (the backend finished and closed the response normally) is treated as\ncompletion, not a failure — it does *not* reconnect by default,** even though `reconnect` defaults\nto `true`. This matters for the common shape of \"emit some events, mark the last one `done`, close\nthe connection\" (exactly what `eventloom`'s FastAPI adapter does once its producer finishes):\nwithout this distinction, every finite stream would get silently re-fetched and replayed in a loop\nthe moment it finished. `status` becomes `\"closed\"` and `onComplete` fires once. If your backend\ninstead expects the client to keep re-polling after every close, opt into the old behavior with\n`reconnectOnComplete: true`.\n\n## Extensibility\n\n| What to override | How |\n|---|---|\n| Which component renders which event type | `registry.register(type, { renderer })` |\n| Merge strategy (frontend override of backend default) | `{ renderer, strategy: \"...\" }` in registration |\n| Unregistered event fallback | `<StreamView fallback={UnknownEventCard} />` |\n| Reconnect/backoff behavior | `useEventStream`/`<StreamView>` options (`reconnect`, `reconnectOnComplete`, `initialReconnectDelayMs`, `maxReconnectDelayMs`), or use `@akshilmy/eventloom-core`'s `StreamConnection` directly for full control |\n| Transport (SSE vs WebSocket later) | Swap the underlying `StreamConnection` — not yet pluggable at the hook level in v1; use `@akshilmy/eventloom-core` directly if you need this today |\n| Layout/ordering of rendered events | Don't use `<StreamView>` — use `useEventStream` directly and render however you like |\n| Auth headers, credentials | `fetchOptions` (see above) |\n\n## Development\n\n```bash\ncd typescript\nnpm install\nnpm run build --workspace=@akshilmy/eventloom-core   # react depends on core's build output\nnpm run build --workspace=@akshilmy/eventloom-react\nnpm run test --workspace=@akshilmy/eventloom-react\nnpm run typecheck --workspace=@akshilmy/eventloom-react\n```\n\n## License\n\nMIT\n","readmeFilename":"README.md","_rev":"1-01cf453c55f2db72e94f269082593c19"}