@urun-sh/react
v0.2.18
Published
React bindings for the urun TypeScript SDK
Maintainers
Readme
@urun-sh/react
React bindings for TypeScript clients that proxy apps deployed on urun from Python.
Install
npm install @urun-sh/react @urun-sh/coreQuick start
import '@urun-sh/react/styles.css'
import { UrunProvider, useApp } from '@urun-sh/react'
import { UrunWorkOSProvider } from '@urun-sh/react/workos'
import { useEffect, useRef } from 'react'
function VideoPlayer() {
const app = useApp()
const session = app.generate({ prompt: 'A sunset' })
const video = session.stream('video')
const videoRef = useRef<HTMLVideoElement>(null)
useEffect(() => {
if (video.track && videoRef.current) {
videoRef.current.srcObject = new MediaStream([video.track])
}
}, [video.track])
return (
<button onClick={() => session.doc('control').set({ desired: { prompt: { text: 'A forest' } } })}>
Generate
</button>
)
}
export default function App() {
return (
<UrunWorkOSProvider clientId="client_...">
<UrunProvider
baseUrl="https://urun.sh"
orgId="your-org-id"
authProvider="workos"
appId="my-app"
>
<VideoPlayer />
</UrunProvider>
</UrunWorkOSProvider>
)
}useApp() is render-safe: repeated app.generate(args) calls during React re-renders reuse the same session for the same function and args.
Drive the app by writing desired state into the control doc (
doc('control').set({ desired: … })), not an imperativestart/update/stop. The Python runtime reads cheap config inline from the livedoc.desired.*props and usesdoc.bind(construct, [live props])(ReactuseMemo) to reconstruct a stage on structural change — the single reconcile lane (no field-mapping). Streams are symmetric:session.stream(name)consumes the runtime's output, and.attach(track)makes the client the producer of that name. See the rootREADME.md"How this mirrors the Python runtime DSL" and theurun-sdk-canonical-patternsskill.
Authentication
Browser clients authenticate with an organization ID plus a user/session JWT. During urun setup, the organization is provisioned with an auth provider configuration, such as a WorkOS client ID mapped to a JWKS URL.
When authProvider="workos", wrap UrunProvider in an auth bridge that supplies short-lived access tokens. For pure React WorkOS apps, import UrunWorkOSProvider from @urun-sh/react/workos. For Next.js WorkOS apps, import UrunWorkOSProvider from @urun-sh/react/next-workos.
For other auth providers, use UrunAuthProvider with a getAccessToken function, or UrunJwtProvider for a static JWT in tests. urun validates JWTs server-side against the JWKS configured for the organization. Do not pass server API keys or long-lived secrets to browser clients.
API
UrunProvider— configures the app proxy.useApp()— returns the deployed app proxy configured byappId.app.<function>(args?)— starts or reconnects a running function call and returns a render-safe session proxy.session.stream(name)— returns a render-safe named symmetric stream (first use decides direction) with.track,.attach(track),.detach(),.seek(seconds | 'live'), and lifecycle events.session.doc(name)— returns a render-safe synced document with.get(),.set(patch)(writedesired.*state), and lifecycle events.registerComponent()and built-in components — optional generative UI support.
Session docs as a zustand store
Session docs (prompts, desired state, status) read like a plain zustand store. The doc — a vanilla Yjs document synced by the platform — stays the single source of truth: the store is a read-projection plus write-through, never an authoritative copy. Selectors give granular re-renders; set(patch) deep-merges through the doc's field-granular CRDT write path, so concurrent writers to different subkeys both survive.
import { useDocStore } from '@urun-sh/react'
type ControlDoc = {
session?: { status?: string }
desired?: { prompt?: { text?: string } }
}
function PromptControls({ session }) {
const useControl = useDocStore<ControlDoc>(session, 'control')
// Status field read — re-renders ONLY when the selected value changes.
const status = useControl((s) => s.doc.session?.status ?? 'idle')
// Desired-state write — write-through to the doc (field-granular merge).
const set = useControl((s) => s.set)
return (
<div>
<span>{status}</span>
<button onClick={() => set({ desired: { prompt: { text: 'a sunset' } } })}>
Set prompt
</button>
</div>
)
}Outside React (or with a session handle you own), bind a store directly to a doc:
import { createDocStore } from '@urun-sh/react'
const control = createDocStore<ControlDoc>(session.doc('control'))
control.getState().doc.session?.status
control.subscribe((s) => console.log(s.doc))
control.set({ desired: { prompt: { text: 'dawn' } } })
control.unbind() // detach the projection; the session still owns the docNotes:
- Selectors that return objects/arrays should use zustand's
useShallow(zustand/react/shallow) — each doc change produces a fresh snapshot tree. - Arrays in the doc are read as plain snapshots. Do not read-modify-write an array through
setfor append-only logs; use the platform's stream/text primitives for growing data. useSessionDoc(session, key, selector?)also accepts a selector for one-off reads.
Styling
Import the package CSS when using built-in components:
import '@urun-sh/react/styles.css'Peer dependencies
@urun-sh/react supports React 18 and 19 and expects @urun-sh/core of the same release line. WorkOS React and Next.js integrations are optional peers used only by @urun-sh/react/workos and @urun-sh/react/next-workos.
