Skip to content

feat(react-hooks): add useSession hook for reading session channels - #4808

Closed
claude[bot] wants to merge 2 commits into
mainfrom
feat/use-session-react-hook
Closed

feat(react-hooks): add useSession hook for reading session channels#4808
claude[bot] wants to merge 2 commits into
mainfrom
feat/use-session-react-hook

Conversation

@claude

@claude claude Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Requested by Matt Aitken · Slack thread

✅ Checklist

  • I have followed every step in the contributing guide
  • The PR title follows the convention.
  • I ran and tested the code works

Testing

Exercised from a React app against a session created by an agent task: render a component calling useSession(sessionId, { accessToken }), confirm records accumulates as the agent writes to the out channel, and that lastControl fires on turn-complete. Then repeat with { io: "in" } to read the input channel, unmount mid-stream to confirm the request is aborted, and pass a saved lastEventId back in to confirm the channel resumes from that cursor instead of replaying from the beginning.


Changelog

Before: reading a session's channel from React meant reaching for the API client directly. You created the client yourself, called the session stream subscription, and then wrote all of the surrounding React plumbing by hand — accumulating records into state, aborting the request when the component unmounted, throttling updates so a chatty channel didn't re-render on every record, and tracking the cursor yourself if you wanted to resume. Every app that showed session output rebuilt the same code, and none of it shared state with the other realtime hooks.

After: useSession does that work, and behaves like the realtime hooks already in the package. It subscribes on mount (or when enabled flips to true), stops on unmount or when you call stop(), and hands back the records received so far, the last control record, the cursor of the last record seen, and any error. io picks which channel to read, defaulting to "out", and lastEventId resumes a channel where a previous session left off.

function useSession<TRecord = unknown>(
  sessionIdOrExternalId?: string,
  options?: UseSessionOptions<TRecord>
): UseSessionInstance<TRecord>;

UseSessionInstance is { records, lastEventId, lastControl, error, stop }. UseSessionOptions extends the shared api-client options with id, enabled, io, throttleInMs, timeoutInSeconds, lastEventId, onRecord and onControl.

How: the hook wraps the API client's existing session stream subscription rather than introducing a new transport. Record state lives in SWR under the same style of key the realtime hooks use, so instances sharing an id share state; the request lifecycle is an AbortController torn down on unmount, on stop(), and when the target channel changes; and incoming records go through the same throttled batching queue, so a busy channel coalesces into one render per throttleInMs (16ms by default). ControlEvent is now re-exported from core so onControl can be typed by consumers, and useStableRequestCallback moved into src/utils/ so useRealtime and useSession share the one copy. A patch changeset for @trigger.dev/react-hooks is included.

Known limitation: access tokens are static here — there is no refresh path, so a long-lived subscription will fail once its token expires. A follow-up will address that.


Screenshots

n/a — no UI surface.

💯

@changeset-bot

changeset-bot Bot commented Aug 27, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3873692

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 27 packages
Name Type
@trigger.dev/react-hooks Patch
@trigger.dev/build Patch
@trigger.dev/core Patch
@trigger.dev/python Patch
@trigger.dev/redis-worker Patch
@trigger.dev/rsc Patch
@trigger.dev/schema-to-json Patch
@trigger.dev/sdk Patch
@trigger.dev/database Patch
@trigger.dev/otlp-importer Patch
@trigger.dev/rbac Patch
@trigger.dev/sso Patch
trigger.dev Patch
@internal/dashboard-agent Patch
@internal/clickhouse Patch
@internal/llm-model-catalog Patch
@internal/metrics-pipeline Patch
@internal/redis Patch
@internal/replication Patch
@internal/run-engine Patch
@internal/run-store Patch
@internal/schedule-engine Patch
@internal/tracing Patch
@internal/webhook-engine Patch
@internal/webhook-sources Patch
@internal/testcontainers Patch
@internal/cache Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@matt-aitken
matt-aitken marked this pull request as ready for review August 28, 2026 00:04

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Devin Review

@claude

claude Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

Preview packages for this PR's head commit 3873692 were published to pkg.pr.new — the bot didn't comment because the preview workflow triggers on push and the branch was pushed before this PR was opened.

npm i https://pkg.pr.new/@trigger.dev/core@3873692
npm i https://pkg.pr.new/@trigger.dev/react-hooks@3873692

These builds are pinned to that head commit; a new push publishes fresh ones and the bot will comment with them itself.


Generated by Claude Code

@ericallam

Copy link
Copy Markdown
Member

Closing in favor of a combined realtime-streams PR that folds this hook in alongside the start-from-latest subscribe option and the token-refresh work from #4809.

As part of that consolidation the hook is being renamed from useSession to useSessionStream, since it reads a session channel. useSession is being reserved for future two-way (read and write) communication.

@ericallam ericallam closed this Aug 28, 2026
ericallam added a commit that referenced this pull request Aug 28, 2026
…#4811)

## Summary

Realtime streams get a live "last value" mode: subscribe from the latest
record instead of replaying the whole history, keep memory bounded, and
resume across reloads. Plus a new `useSessionStream` hook for reading a
Session's channels from React.

## `useRealtimeStream`: start-from-latest, bounded, resumable

```tsx
const { parts, lastEventId } = useRealtimeStream<Frame>(runId, "frames", {
  from: "latest",   // skip history, only new records after connect
  maxParts: 1,      // keep just the most recent (bounded memory)
  lastEventId: saved, // resume from a persisted cursor (survives reload)
  onParts: (batch) => save(batch.at(-1)?.id), // per-batch event ids
  accessToken,
});
```

`from`, `lastEventId` (option and return), and the batching also apply
to `streams.read()` and `fetchStream()`.

## `useSessionStream`: read a Session channel from React (new)

A read-only hook for a Session's `out` (default) or `in` channel, with
the same start / bound / resume options. `useSession` is reserved for
two-way (read and write).

```tsx
const { records, lastEventId } = useSessionStream<Frame>(sessionId, {
  io: "out",
  from: "latest",
  maxRecords: 5,
  onRecords: (batch) => {/* each throttled batch, with event ids */},
  accessToken,
});
```

## Access-token refresh

Long-lived subscriptions can survive token expiry: pass
`refreshAccessToken` and a 401/403 triggers one re-mint and reconnect.
With no refresher, auth errors stay terminal exactly as before.

```tsx
const { parts } = useRealtimeStream<Frame>(runId, "frames", {
  accessToken,
  // called on a 401/403 to mint a fresh public token from your backend
  refreshAccessToken: async () => {
    const res = await fetch("/api/realtime-token");
    return (await res.json()).token;
  },
});
```

It is also available on `useApiClient` / `TriggerAuthContext`, so every
hook under a provider shares one refresher.

## Notes

Server support (S2 `tail_offset` / Redis `$`, and the start-position
header on the run and session SSE routes) ships here; a client passing
`from: "latest"` against an older server degrades safely to a full
replay. Resume, bounded memory, batched callbacks, and token refresh are
client-only.

Supersedes #4808 and #4809, folded in here. Verified end to end on an
isolated stack: `from: "latest"` on the run and session paths against
real S2, `lastEventId` resume across a reload, bounded memory, batched
callbacks, and a real 401 to token-refresh to reconnect.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants