cloudflare / cloudflare/agents

Support non-suspending hook + exportable fetch function for framework loaders

Open
#1,011 2 comments 3 reactions 1 assignee Claimed by @threepointone View on GitHub
enhancement
Dominant language
TypeScript
Stars
5.6k
Forks
711
Avg merge
1d 20h
Merged PRs (30d)
53

Description

## Problem

`useAgentChat` forces React Suspense via `use()` for initial message fetching. This creates two architectural constraints:

1. **Fetch location = Suspense boundary location.** You can't fetch messages in a parent component and handle loading/error states in a child. If the component calling `useAgentChat` unmounts (e.g., panel toggle), it re-suspends on remount — there's no way to keep the fetch alive independently.

2. **No exported fetch function.** Modern frameworks prefetch data in route loaders to eliminate waterfalls. The library's message fetch is locked inside `useAgentChat` — there's no standalone function to call from a loader. It's possible to work around this by constructing the messages URL manually and feeding the result via `getInitialMessages: null` + `messages`, but this depends on knowing the internal URL shape and isn't a supported pattern. Exporting something like `defaultGetInitialMessagesFetch` would make this a first-class workflow.

The `getInitialMessages: null` + `messages` option bypasses `use()`, but there's no clean public API for non-suspending consumption.

## Proposal

Two additions:

1. **Export a framework-agnostic fetch function** that can be called in any route loader to prefetch messages before the component tree mounts.

2. **Offer both non-suspending and suspending hooks**, following the `useQuery` / `useSuspenseQuery` convention from TanStack Query.

## API sketch

> Just a rough sketch to illustrate the shape — not prescriptive.

```typescript
// 1. Standalone fetch — use in any framework's loader
import { getAgentMessages } from '@cloudflare/ai-chat'

// TanStack Start / Router
loader: async () => {
const messages = await getAgentMessages({
host,
agent,
name,
query: { workflowId }, // same params as useAgent's query
})
return { messages }
}

// Next.js, Remix — same function, different loader shape

// 2. Non-suspending hook — consumer handles loading/error
import { useAgentChat } from '@cloudflare/ai-chat/react'

const { messages, isPending, error } = useAgentChat({
agent,
initialMessages, // from loader or cache
})

// 3. Suspending hook — current behavior, explicit opt-in
import { useSuspenseAgentChat } from '@cloudflare/ai-chat/react'

const { messages } = useSuspenseAgentChat({ agent })
```

## Benefits

- **Familiar patterns.** Developers already use `useQuery` / `useSuspenseQuery` in TanStack Query and loader-based prefetching in TanStack Start, Next.js, and Remix. Same mental model, no learning curve.
- **Framework-agnostic.** A standalone fetch function works in any framework's loader — no coupling to React Suspense or a specific router.
- **Efficient loading.** Messages prefetch in parallel with other loader data instead of waiting for component mount. No waterfalls.
- **Decoupled UI.** Consumers choose independently where to fetch and where to show loading/error states. Panel toggles, tab switches, and layout changes don't trigger refetches.

## Implementation notes

The building blocks already exist internally:

- `defaultGetInitialMessagesFetch` is the standalone fetch function — it just needs to be exported.
- The `getInitialMessages: null` + `messages` option already bypasses `use()` — the non-suspending hook would wrap this path with `{ isPending, error }` tracking.
- `useSuspenseAgentChat` is the current `useAgentChat` renamed for clarity.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.