675 lines
19 KiB
Plaintext
675 lines
19 KiB
Plaintext
---
|
|
title: "Run updates in React"
|
|
sidebarTitle: "Run updates"
|
|
description: "Build progress bars, status indicators, and live dashboards by subscribing to background task updates from React components."
|
|
---
|
|
|
|
**Subscribe to a run and your component re-renders whenever its status, metadata, or tags change.** Build progress bars, deployment monitors, or any UI that needs to reflect what a background task is doing right now.
|
|
|
|
For streaming continuous data (AI tokens, file chunks), see [Streaming](/realtime/react-hooks/streams) instead.
|
|
|
|
## Trigger + subscribe combo hooks
|
|
|
|
Trigger a task and immediately subscribe to its run. Details in the [triggering](/realtime/react-hooks/triggering) section.
|
|
|
|
- **[`useRealtimeTaskTrigger`](/realtime/react-hooks/triggering#userealtimetasktrigger)** - Trigger a task and subscribe to the run
|
|
- **[`useRealtimeTaskTriggerWithStreams`](/realtime/react-hooks/triggering#userealtimetasktriggerwithstreams)** - Trigger a task and subscribe to both run updates and streams
|
|
|
|
## Subscribe hooks
|
|
|
|
### useRealtimeRun
|
|
|
|
The `useRealtimeRun` hook allows you to subscribe to a run by its ID.
|
|
|
|
```tsx
|
|
"use client"; // This is needed for Next.js App Router or other RSC frameworks
|
|
|
|
import { useRealtimeRun } from "@trigger.dev/react-hooks";
|
|
|
|
export function MyComponent({
|
|
runId,
|
|
publicAccessToken,
|
|
}: {
|
|
runId: string;
|
|
publicAccessToken: string;
|
|
}) {
|
|
const { run, error } = useRealtimeRun(runId, {
|
|
accessToken: publicAccessToken,
|
|
});
|
|
|
|
if (error) return <div>Error: {error.message}</div>;
|
|
|
|
return <div>Run: {run.id}</div>;
|
|
}
|
|
```
|
|
|
|
To correctly type the run's payload and output, you can provide the type of your task to the `useRealtimeRun` hook:
|
|
|
|
```tsx
|
|
import { useRealtimeRun } from "@trigger.dev/react-hooks";
|
|
import type { myTask } from "@/trigger/myTask";
|
|
|
|
export function MyComponent({
|
|
runId,
|
|
publicAccessToken,
|
|
}: {
|
|
runId: string;
|
|
publicAccessToken: string;
|
|
}) {
|
|
const { run, error } = useRealtimeRun<typeof myTask>(runId, {
|
|
accessToken: publicAccessToken,
|
|
});
|
|
|
|
if (error) return <div>Error: {error.message}</div>;
|
|
|
|
// Now run.payload and run.output are correctly typed
|
|
|
|
return <div>Run: {run.id}</div>;
|
|
}
|
|
```
|
|
|
|
You can supply an `onComplete` callback to the `useRealtimeRun` hook to be called when the run is completed or errored. This is useful if you want to perform some action when the run is completed, like navigating to a different page or showing a notification.
|
|
|
|
```tsx
|
|
import { useRealtimeRun } from "@trigger.dev/react-hooks";
|
|
|
|
export function MyComponent({
|
|
runId,
|
|
publicAccessToken,
|
|
}: {
|
|
runId: string;
|
|
publicAccessToken: string;
|
|
}) {
|
|
const { run, error } = useRealtimeRun(runId, {
|
|
accessToken: publicAccessToken,
|
|
onComplete: (run, error) => {
|
|
console.log("Run completed", run);
|
|
},
|
|
});
|
|
|
|
if (error) return <div>Error: {error.message}</div>;
|
|
|
|
return <div>Run: {run.id}</div>;
|
|
}
|
|
```
|
|
|
|
When you only need run status (for example, a progress bar or completion badge), you can omit large fields like `payload` and `output` by passing `skipColumns`. This reduces the data sent over the wire and avoids issues such as "Large HTTP Payload" warnings in tools like Sentry.
|
|
|
|
```tsx
|
|
import { useRealtimeRun } from "@trigger.dev/react-hooks";
|
|
|
|
export function RunStatusBadge({
|
|
runId,
|
|
publicAccessToken,
|
|
}: {
|
|
runId: string;
|
|
publicAccessToken: string;
|
|
}) {
|
|
const { run, error } = useRealtimeRun(runId, {
|
|
accessToken: publicAccessToken,
|
|
skipColumns: ["payload", "output"],
|
|
});
|
|
|
|
if (error) return <span>Error</span>;
|
|
if (!run) return <span>Loading…</span>;
|
|
|
|
return <span>Status: {run.status}</span>;
|
|
}
|
|
```
|
|
|
|
You can skip any of: `payload`, `output`, `metadata`, `startedAt`, `delayUntil`, `queuedAt`, `expiredAt`, `completedAt`, `number`, `isTest`, `usageDurationMs`, `costInCents`, `baseCostInCents`, `ttl`, `payloadType`, `outputType`, `runTags`, `error`. The `useRealtimeRunsWithTag` hook also accepts a `skipColumns` option in the same way.
|
|
|
|
See our [run object reference](/realtime/run-object) for the complete schema and [How it Works documentation](/realtime/how-it-works) for more technical details.
|
|
|
|
### useRealtimeRunsWithTag
|
|
|
|
The `useRealtimeRunsWithTag` hook allows you to subscribe to multiple runs with a specific tag.
|
|
|
|
```tsx
|
|
"use client"; // This is needed for Next.js App Router or other RSC frameworks
|
|
|
|
import { useRealtimeRunsWithTag } from "@trigger.dev/react-hooks";
|
|
|
|
export function MyComponent({ tag }: { tag: string }) {
|
|
const { runs, error } = useRealtimeRunsWithTag(tag);
|
|
|
|
if (error) return <div>Error: {error.message}</div>;
|
|
|
|
return (
|
|
<div>
|
|
{runs.map((run) => (
|
|
<div key={run.id}>Run: {run.id}</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
To correctly type the runs payload and output, you can provide the type of your task to the `useRealtimeRunsWithTag` hook:
|
|
|
|
```tsx
|
|
import { useRealtimeRunsWithTag } from "@trigger.dev/react-hooks";
|
|
import type { myTask } from "@/trigger/myTask";
|
|
|
|
export function MyComponent({ tag }: { tag: string }) {
|
|
const { runs, error } = useRealtimeRunsWithTag<typeof myTask>(tag);
|
|
|
|
if (error) return <div>Error: {error.message}</div>;
|
|
|
|
// Now runs[i].payload and runs[i].output are correctly typed
|
|
|
|
return (
|
|
<div>
|
|
{runs.map((run) => (
|
|
<div key={run.id}>Run: {run.id}</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
If `useRealtimeRunsWithTag` could return multiple different types of tasks, you can pass a union of all the task types to the hook:
|
|
|
|
```tsx
|
|
import { useRealtimeRunsWithTag } from "@trigger.dev/react-hooks";
|
|
import type { myTask1, myTask2 } from "@/trigger/myTasks";
|
|
|
|
export function MyComponent({ tag }: { tag: string }) {
|
|
const { runs, error } = useRealtimeRunsWithTag<typeof myTask1 | typeof myTask2>(tag);
|
|
|
|
if (error) return <div>Error: {error.message}</div>;
|
|
|
|
// You can narrow down the type of the run based on the taskIdentifier
|
|
for (const run of runs) {
|
|
if (run.taskIdentifier === "my-task-1") {
|
|
// run is correctly typed as myTask1
|
|
} else if (run.taskIdentifier === "my-task-2") {
|
|
// run is correctly typed as myTask2
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
{runs.map((run) => (
|
|
<div key={run.id}>Run: {run.id}</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### useRealtimeBatch
|
|
|
|
The `useRealtimeBatch` hook allows you to subscribe to a batch of runs by its the batch ID.
|
|
|
|
```tsx
|
|
"use client"; // This is needed for Next.js App Router or other RSC frameworks
|
|
|
|
import { useRealtimeBatch } from "@trigger.dev/react-hooks";
|
|
|
|
export function MyComponent({ batchId }: { batchId: string }) {
|
|
const { runs, error } = useRealtimeBatch(batchId);
|
|
|
|
if (error) return <div>Error: {error.message}</div>;
|
|
|
|
return (
|
|
<div>
|
|
{runs.map((run) => (
|
|
<div key={run.id}>Run: {run.id}</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
See our [Realtime documentation](/realtime) for more information.
|
|
|
|
## Using metadata to show progress in your UI
|
|
|
|
All realtime hooks automatically include metadata updates. Whenever your task updates metadata using `metadata.set()`, `metadata.append()`, or other metadata methods, your component will re-render with the updated data.
|
|
|
|
<Note>To learn how to write tasks using metadata, see our [metadata](/runs/metadata) guide.</Note>
|
|
|
|
### Progress monitoring
|
|
|
|
This example demonstrates how to create a progress monitor component that can be used to display the progress of a run:
|
|
|
|
```tsx
|
|
"use client"; // This is needed for Next.js App Router or other RSC frameworks
|
|
|
|
import { useRealtimeRun } from "@trigger.dev/react-hooks";
|
|
|
|
export function ProgressMonitor({
|
|
runId,
|
|
publicAccessToken,
|
|
}: {
|
|
runId: string;
|
|
publicAccessToken: string;
|
|
}) {
|
|
const { run, error, isLoading } = useRealtimeRun(runId, {
|
|
accessToken: publicAccessToken,
|
|
});
|
|
|
|
if (isLoading) return <div>Loading run...</div>;
|
|
if (error) return <div>Error: {error.message}</div>;
|
|
if (!run) return <div>Run not found</div>;
|
|
|
|
const progress = run.metadata?.progress as
|
|
| {
|
|
current: number;
|
|
total: number;
|
|
percentage: number;
|
|
currentItem: string;
|
|
}
|
|
| undefined;
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<h3>Run Status: {run.status}</h3>
|
|
<p>Run ID: {run.id}</p>
|
|
</div>
|
|
|
|
{progress && (
|
|
<div className="space-y-2">
|
|
<div className="flex justify-between text-sm">
|
|
<span>Progress</span>
|
|
<span>{progress.percentage}%</span>
|
|
</div>
|
|
<div className="w-full bg-gray-200 rounded-full h-2">
|
|
<div
|
|
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
|
style={{ width: `${progress.percentage}%` }}
|
|
/>
|
|
</div>
|
|
<p className="text-sm text-gray-600">
|
|
Processing: {progress.currentItem} ({progress.current}/{progress.total})
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### Reusable progress bar
|
|
|
|
This example demonstrates how to create a reusable progress bar component that can be used to display the percentage progress of a run:
|
|
|
|
```tsx
|
|
"use client";
|
|
|
|
import { useRealtimeRun } from "@trigger.dev/react-hooks";
|
|
|
|
interface ProgressBarProps {
|
|
runId: string;
|
|
publicAccessToken: string;
|
|
title?: string;
|
|
}
|
|
|
|
export function ProgressBar({ runId, publicAccessToken, title }: ProgressBarProps) {
|
|
const { run } = useRealtimeRun(runId, {
|
|
accessToken: publicAccessToken,
|
|
});
|
|
|
|
const progress = run?.metadata?.progress as
|
|
| {
|
|
current?: number;
|
|
total?: number;
|
|
percentage?: number;
|
|
currentItem?: string;
|
|
}
|
|
| undefined;
|
|
|
|
const percentage = progress?.percentage ?? 0;
|
|
const isComplete = run?.status === "COMPLETED";
|
|
const isFailed = run?.status === "FAILED";
|
|
|
|
return (
|
|
<div className="w-full space-y-2">
|
|
{title && <h4 className="font-medium">{title}</h4>}
|
|
|
|
<div className="w-full bg-gray-200 rounded-full h-3">
|
|
<div
|
|
className={`h-3 rounded-full transition-all duration-500 ${
|
|
isFailed ? "bg-red-500" : isComplete ? "bg-green-500" : "bg-blue-500"
|
|
}`}
|
|
style={{ width: `${percentage}%` }}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex justify-between text-sm text-gray-600">
|
|
<span>
|
|
{progress?.current && progress?.total
|
|
? `${progress.current}/${progress.total} items`
|
|
: "Processing..."}
|
|
</span>
|
|
<span>{percentage}%</span>
|
|
</div>
|
|
|
|
{progress?.currentItem && (
|
|
<p className="text-sm text-gray-500 truncate">Current: {progress.currentItem}</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### Status indicator with logs
|
|
|
|
This example demonstrates how to create a status indicator component that can be used to display the status of a run, and also logs that are emitted by the task:
|
|
|
|
```tsx
|
|
"use client";
|
|
|
|
import { useRealtimeRun } from "@trigger.dev/react-hooks";
|
|
|
|
interface StatusIndicatorProps {
|
|
runId: string;
|
|
publicAccessToken: string;
|
|
}
|
|
|
|
export function StatusIndicator({ runId, publicAccessToken }: StatusIndicatorProps) {
|
|
const { run } = useRealtimeRun(runId, {
|
|
accessToken: publicAccessToken,
|
|
});
|
|
|
|
const status = run?.metadata?.status as string | undefined;
|
|
const logs = run?.metadata?.logs as string[] | undefined;
|
|
|
|
const getStatusColor = (status: string | undefined) => {
|
|
switch (status) {
|
|
case "completed":
|
|
return "text-green-600 bg-green-100";
|
|
case "failed":
|
|
return "text-red-600 bg-red-100";
|
|
case "running":
|
|
return "text-blue-600 bg-blue-100";
|
|
default:
|
|
return "text-gray-600 bg-gray-100";
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center space-x-2">
|
|
<span className={`px-3 py-1 rounded-full text-sm font-medium ${getStatusColor(status)}`}>
|
|
{status || run?.status || "Unknown"}
|
|
</span>
|
|
<span className="text-sm text-gray-500">Run {run?.id}</span>
|
|
</div>
|
|
|
|
{logs && logs.length > 0 && (
|
|
<div className="bg-gray-50 rounded-lg p-4">
|
|
<h4 className="font-medium mb-2">Logs</h4>
|
|
<div className="space-y-1 max-h-48 overflow-y-auto">
|
|
{logs.map((log, index) => (
|
|
<div key={index} className="text-sm text-gray-700 font-mono">
|
|
{log}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### Multi-stage deployment monitor
|
|
|
|
This example demonstrates how to create a multi-stage deployment monitor component that can be used to display the progress of a deployment:
|
|
|
|
```tsx
|
|
"use client";
|
|
|
|
import { useRealtimeRun } from "@trigger.dev/react-hooks";
|
|
|
|
interface DeploymentMonitorProps {
|
|
runId: string;
|
|
publicAccessToken: string;
|
|
}
|
|
|
|
const DEPLOYMENT_STAGES = [
|
|
"initializing",
|
|
"building",
|
|
"testing",
|
|
"deploying",
|
|
"verifying",
|
|
"completed",
|
|
] as const;
|
|
|
|
export function DeploymentMonitor({ runId, publicAccessToken }: DeploymentMonitorProps) {
|
|
const { run } = useRealtimeRun(runId, {
|
|
accessToken: publicAccessToken,
|
|
});
|
|
|
|
const status = run?.metadata?.status as string | undefined;
|
|
const logs = run?.metadata?.logs as string[] | undefined;
|
|
const currentStageIndex = DEPLOYMENT_STAGES.indexOf(status as any);
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<h3 className="text-lg font-semibold">Deployment Progress</h3>
|
|
|
|
{/* Stage indicators */}
|
|
<div className="space-y-4">
|
|
{DEPLOYMENT_STAGES.map((stage, index) => {
|
|
const isActive = currentStageIndex === index;
|
|
const isCompleted = currentStageIndex > index;
|
|
const isFailed = run?.status === "FAILED" && currentStageIndex === index;
|
|
|
|
return (
|
|
<div key={stage} className="flex items-center space-x-3">
|
|
<div
|
|
className={`w-6 h-6 rounded-full flex items-center justify-center text-sm font-medium ${
|
|
isFailed
|
|
? "bg-red-500 text-white"
|
|
: isCompleted
|
|
? "bg-green-500 text-white"
|
|
: isActive
|
|
? "bg-blue-500 text-white"
|
|
: "bg-gray-200 text-gray-600"
|
|
}`}
|
|
>
|
|
{isCompleted ? "✓" : index + 1}
|
|
</div>
|
|
<span
|
|
className={`capitalize ${
|
|
isActive
|
|
? "font-medium text-blue-600"
|
|
: isCompleted
|
|
? "text-green-600"
|
|
: isFailed
|
|
? "text-red-600"
|
|
: "text-gray-500"
|
|
}`}
|
|
>
|
|
{stage}
|
|
</span>
|
|
{isActive && (
|
|
<div className="animate-spin w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full" />
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Recent logs */}
|
|
{logs && logs.length > 0 && (
|
|
<div className="bg-black text-green-400 rounded-lg p-4 font-mono text-sm">
|
|
<div className="space-y-1 max-h-32 overflow-y-auto">
|
|
{logs.slice(-5).map((log, index) => (
|
|
<div key={index}>
|
|
<span className="text-gray-500">$ </span>
|
|
{log}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### Type safety
|
|
|
|
Define TypeScript interfaces for your metadata to get full type safety:
|
|
|
|
```tsx
|
|
"use client";
|
|
|
|
import { useRealtimeRun } from "@trigger.dev/react-hooks";
|
|
|
|
interface TaskMetadata {
|
|
progress?: {
|
|
current: number;
|
|
total: number;
|
|
percentage: number;
|
|
currentItem: string;
|
|
};
|
|
status?: "initializing" | "processing" | "completed" | "failed";
|
|
user?: {
|
|
id: string;
|
|
name: string;
|
|
};
|
|
logs?: string[];
|
|
}
|
|
|
|
export function TypedMetadataComponent({
|
|
runId,
|
|
publicAccessToken,
|
|
}: {
|
|
runId: string;
|
|
publicAccessToken: string;
|
|
}) {
|
|
const { run } = useRealtimeRun(runId, {
|
|
accessToken: publicAccessToken,
|
|
});
|
|
|
|
// Type-safe metadata access
|
|
const metadata = run?.metadata as TaskMetadata | undefined;
|
|
|
|
return (
|
|
<div>
|
|
{metadata?.progress && <p>Progress: {metadata.progress.percentage}%</p>}
|
|
|
|
{metadata?.user && (
|
|
<p>
|
|
User: {metadata.user.name} ({metadata.user.id})
|
|
</p>
|
|
)}
|
|
|
|
{metadata?.status && <p>Status: {metadata.status}</p>}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
## Common options
|
|
|
|
### accessToken & baseURL
|
|
|
|
You can pass the `accessToken` option to the Realtime hooks to authenticate the subscription.
|
|
|
|
```tsx
|
|
import { useRealtimeRun } from "@trigger.dev/react-hooks";
|
|
|
|
export function MyComponent({
|
|
runId,
|
|
publicAccessToken,
|
|
}: {
|
|
runId: string;
|
|
publicAccessToken: string;
|
|
}) {
|
|
const { run, error } = useRealtimeRun(runId, {
|
|
accessToken: publicAccessToken,
|
|
baseURL: "https://my-self-hosted-trigger.com", // Optional if you are using a self-hosted Trigger.dev instance
|
|
});
|
|
|
|
if (error) return <div>Error: {error.message}</div>;
|
|
|
|
return <div>Run: {run.id}</div>;
|
|
}
|
|
```
|
|
|
|
### enabled
|
|
|
|
You can pass the `enabled` option to the Realtime hooks to enable or disable the subscription.
|
|
|
|
```tsx
|
|
import { useRealtimeRun } from "@trigger.dev/react-hooks";
|
|
|
|
export function MyComponent({
|
|
runId,
|
|
publicAccessToken,
|
|
enabled,
|
|
}: {
|
|
runId: string;
|
|
publicAccessToken: string;
|
|
enabled: boolean;
|
|
}) {
|
|
const { run, error } = useRealtimeRun(runId, {
|
|
accessToken: publicAccessToken,
|
|
enabled,
|
|
});
|
|
|
|
if (error) return <div>Error: {error.message}</div>;
|
|
|
|
return <div>Run: {run.id}</div>;
|
|
}
|
|
```
|
|
|
|
This allows you to conditionally disable using the hook based on some state.
|
|
|
|
### id
|
|
|
|
You can pass the `id` option to the Realtime hooks to change the ID of the subscription.
|
|
|
|
```tsx
|
|
import { useRealtimeRun } from "@trigger.dev/react-hooks";
|
|
|
|
export function MyComponent({
|
|
id,
|
|
runId,
|
|
publicAccessToken,
|
|
enabled,
|
|
}: {
|
|
id: string;
|
|
runId: string;
|
|
publicAccessToken: string;
|
|
enabled: boolean;
|
|
}) {
|
|
const { run, error } = useRealtimeRun(runId, {
|
|
accessToken: publicAccessToken,
|
|
enabled,
|
|
id,
|
|
});
|
|
|
|
if (error) return <div>Error: {error.message}</div>;
|
|
|
|
return <div>Run: {run.id}</div>;
|
|
}
|
|
```
|
|
|
|
This allows you to change the ID of the subscription based on some state. Passing in a different ID will unsubscribe from the current subscription and subscribe to the new one (and remove any cached data).
|
|
|
|
## Frequently asked questions
|
|
|
|
### How do I show a progress bar for a background task in React?
|
|
|
|
Use `metadata.set()` inside your task to update a progress value, then read it with `useRealtimeRun` in your component. The hook re-renders your component on every metadata change. See [Using metadata to show progress](#using-metadata-to-show-progress-in-your-ui) above for a complete example.
|
|
|
|
### What's the difference between run updates and streaming?
|
|
|
|
Run updates (this page) give you **run state**: status, metadata, and tags. They're for progress bars, status badges, and dashboards. [Streaming](/realtime/react-hooks/streams) gives you **continuous data** like AI tokens or file chunks. Use run updates for "how far along is my task?" and streaming for "show me the output as it generates."
|
|
|
|
### Can I subscribe to multiple runs at once?
|
|
|
|
Yes. Use `useRealtimeRunsWithTag` to subscribe to all runs with a specific tag (e.g., `user:123`), or `useRealtimeBatch` for all runs in a batch. Each yields an array of run objects that update in real time.
|
|
|
|
### Do I need to set up polling or WebSockets?
|
|
|
|
No. The hooks handle the connection automatically using Trigger.dev's Realtime infrastructure (built on [Electric SQL](/realtime/how-it-works)). Just pass a run ID and an access token.
|