Packages

@surfjs/react

React SDK for Surf Live โ€” real-time state sync hooks and components.

@surfjs/react

React SDK for Surf Live โ€” connect your React app to a Surf WebSocket server and receive real-time state updates from AI agents.

Installation#

bash
npm install @surfjs/react

Peer dependencies: react ^18.0.0 || ^19.0.0

SurfProvider#

Wrap your app with SurfProvider to establish a WebSocket connection:

tsx
import { SurfProvider } from '@surfjs/react'
ย 
function App() {
return (
<SurfProvider
url="wss://myapp.com/surf/ws"
auth="your-auth-token"
channels={['project-123']}
>
<MyComponent />
</SurfProvider>
)
}

Props

| Prop | Type | Description | |------|------|-------------| | url | string | WebSocket URL to connect to | | auth | string? | Auth token sent on connect | | channels | string[]? | Channels to subscribe to on connect |

Auto-Reconnect

The provider automatically reconnects with exponential backoff: 1s โ†’ 2s โ†’ 4s โ†’ 8s โ†’ max 30s.

Channel subscriptions are restored on reconnect.

useSurf#

Access the core Surf context โ€” execute commands and check connection status:

tsx
import { useSurf } from '@surfjs/react'
ย 
function Controls() {
const { execute, connected, status } = useSurf()
ย 
const addClip = async () => {
const result = await execute('video.addClip', { url: 'clip.mp4' })
if (result.ok) console.log('Added:', result.result)
}
ย 
return (
<button onClick={addClip} disabled={!connected}>
Add Clip
</button>
)
}

Returns

| Property | Type | Description | |----------|------|-------------| | execute | (command, params?) => Promise<SurfResult> | Execute a Surf command | | connected | boolean | Whether WebSocket is connected | | status | ConnectionStatus | 'connecting' \| 'connected' \| 'disconnected' \| 'reconnecting' | | sessionId | string? | Current session ID |

useSurfEvent#

Subscribe to real-time events. Automatically cleans up on unmount:

tsx
import { useSurfEvent } from '@surfjs/react'
ย 
function Timeline() {
const [clips, setClips] = useState([])
ย 
useSurfEvent('timeline.updated', (data) => {
setClips(data.clips)
})
ย 
return <ClipList clips={clips} />
}

useSurfState#

Synced state that auto-updates from Surf Live broadcast events:

tsx
import { useSurfState } from '@surfjs/react'
ย 
function Editor() {
const [state, setState] = useSurfState('project-123', {
timeline: { clips: [], playhead: 0 },
selectedClip: null,
})
ย 
// `state` updates automatically when the server calls:
// surf.live.setState('project-123', newState)
// surf.live.patchState('project-123', partialUpdate)
ย 
return (
<div>
<p>Playhead: {state.timeline.playhead}s</p>
<p>Clips: {state.timeline.clips.length}</p>
</div>
)
}

useSurfState handles both full state updates (surf:state) and patches (surf:patch). It uses version numbers to prevent stale updates.

useSurfChannel#

Dynamically manage channel subscriptions:

tsx
import { useSurfChannel } from '@surfjs/react'
ย 
function ProjectSwitcher({ projectId }) {
const { subscribe, unsubscribe, channels } = useSurfChannel()
ย 
useEffect(() => {
subscribe(projectId)
return () => unsubscribe(projectId)
}, [projectId])
ย 
return <p>Subscribed to: {[...channels].join(', ')}</p>
}

Connection Status#

tsx
import { useSurf } from '@surfjs/react'
ย 
function StatusIndicator() {
const { status } = useSurf()
ย 
const colors = {
connecting: 'yellow',
connected: 'green',
disconnected: 'red',
reconnecting: 'orange',
}
ย 
return (
<span style={{ color: colors[status] }}>
{status}
</span>
)
}

TypeScript#

All hooks and components are fully typed. The SurfResult type:

typescript
interface SurfResult {
ok: boolean
result?: unknown
error?: { code: string; message: string }
state?: Record<string, unknown>
}