Integrate in React
Subscribe to live output in the browser with the useCodeRunnerJob hook.
@teovilla/code-runner-react is the browser side of the integration. It wraps
pusher-js to subscribe to a job's
private-run-<jobId> channel over soketi and reassembles ordered stdout/stderr for
you. It carries no secrets and no bearer token — actions like stdin and kill are
delegated back to your backend through callbacks.
Install
pnpm add @teovilla/code-runner-react pusher-jsnpm install @teovilla/code-runner-react pusher-jsyarn add @teovilla/code-runner-react pusher-jsreact >= 18 and pusher-js >= 8 are peer dependencies.
Wrap your app in the provider
CodeRunnerProvider lazily creates a single pusher-js connection and supplies it to
every hook below it.
import { CodeRunnerProvider } from '@teovilla/code-runner-react';
export function App() {
return (
<CodeRunnerProvider
appKey={import.meta.env.VITE_SOKETI_APP_KEY} // public app key
host="localhost" // soketi host
port={6001}
useTLS={false}
authEndpoint="/code-runner/auth" // YOUR backend's channel-auth route
>
<YourRoutes />
</CodeRunnerProvider>
);
}| Prop | Type | Notes |
|---|---|---|
appKey | string | The public soketi app key. Safe in the browser. |
host | string | soketi hostname. |
port | number? | Defaults to 6001. |
useTLS | boolean? | true for wss://. Defaults to false. |
cluster | string? | Leave unset for self-hosted soketi. |
authEndpoint | string | Your backend route that signs private-channel auth. |
authHeaders | Record<string,string>? | Extra headers (session cookie, CSRF) for the auth request. |
authEndpoint points at your backend, not at code-runner. That route uses the
Node SDK's createChannelAuthorizer
to sign the subscription with the app secret. The browser never sees the secret.
Subscribe to a job
import { useCodeRunnerJob } from '@teovilla/code-runner-react';
function JobView({ jobId }: { jobId: string }) {
const { stage, stdout, stderr, compileOutput, result, status, sendStdin, kill } =
useCodeRunnerJob({
jobId,
// The start-handshake: subscribe FIRST, then start once soketi confirms it.
onSubscribed: () => fetch(`/api/jobs/${jobId}/start`, { method: 'POST' }),
// sendStdin / kill don't talk to code-runner directly — they call YOUR backend,
// which holds the bearer token and forwards to the code-runner API.
onStdin: (chunk) =>
fetch(`/api/jobs/${jobId}/stdin`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chunk }),
}),
onKill: () => fetch(`/api/jobs/${jobId}/kill`, { method: 'POST' }),
});
return (
<div>
<p>stage: {stage ?? '—'} · status: {status}</p>
{/* Compiled languages stream a live build log here, separate from the run. */}
{compileOutput && <pre>{compileOutput}</pre>}
<pre>{stdout}</pre>
<pre style={{ color: 'crimson' }}>{stderr}</pre>
<button onClick={() => sendStdin('World\n')}>Send stdin</button>
<button onClick={() => kill()}>Kill</button>
{result && <p>exit {result.exitCode} · {result.durationMs}ms</p>}
</div>
);
}Hook arguments
| Field | Type | Notes |
|---|---|---|
jobId | string | The job to subscribe to. |
channel | string? | Override; defaults to private-run-${jobId}. |
onStdin | (chunk: string) => void | Promise<void> | Called by sendStdin. Route to your backend. |
onKill | () => void | Promise<void> | Called by kill. Route to your backend. |
onSubscribed | () => void | Promise<void> | Fires once soketi confirms the subscription. The correct place to trigger the job's start (the start-handshake). |
Hook return value
| Field | Type | Notes |
|---|---|---|
stage | StagePhase | null | "queued" | "compiling" | "running". |
stdout | string | Reassembled, ordered stdout (program run output only). |
stderr | string | Reassembled, ordered stderr (program run output only). |
compileOutput | string | Live build log of the compile stage (compiled languages only), reassembled in emission order. Separate from stdout/stderr so you can render a dedicated build panel. Empty for interpreted languages, before the compiler emits anything, or on a clean compile with no warnings/errors. |
result | ResultEvent | null | Terminal result once the job ends. |
artifacts | Artifact[] | Captured workspace artifacts (if any). |
status | "idle" | "running" | "done" | Coarse lifecycle state. |
sendStdin | (chunk: string) => void | Promise<void> | Invokes onStdin. |
kill | () => void | Promise<void> | Invokes onKill. |
The hook handles chunk ordering by seq internally, so stdout/stderr (and
compileOutput) are always correctly ordered even if WebSocket frames arrive out
of order.
Start-handshake
A job runs the instant it is started — and can finish in single-digit
milliseconds. If you call start before this client has finished subscribing,
the output is emitted to nobody and you see nothing. So subscribe first, then
start: pass onSubscribed and trigger the job's start (via your backend)
only once soketi confirms the subscription (pusher:subscription_succeeded).
useCodeRunnerJob({
jobId,
onSubscribed: () => fetch(`/api/jobs/${jobId}/start`, { method: 'POST' }),
});The compile-stage build log (compileOutput) arrives on its own compile_output
event, so a clean compile that prints nothing simply leaves compileOutput empty
— there is no run output to confuse it with.
Putting it together
Read Interactive Sessions for the complete
backend + frontend flow, including who calls execute, who subscribes, and who calls
start.