Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-tools-work.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/octane-query': minor
---

Add the official TanStack Query adapter for Octane.
1 change: 1 addition & 0 deletions .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@tanstack/angular-query-experimental",
"@tanstack/angular-query-persist-client",
"@tanstack/eslint-plugin-query",
"@tanstack/octane-query",
"@tanstack/preact-query",
"@tanstack/preact-query-devtools",
"@tanstack/preact-query-persist-client",
Expand Down
39 changes: 39 additions & 0 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,23 @@
"to": "framework/preact/graphql"
}
]
},
{
"label": "octane",
"children": [
{
"label": "Overview",
"to": "framework/octane/overview"
},
{
"label": "Installation",
"to": "framework/octane/installation"
},
{
"label": "Quick Start",
"to": "framework/octane/quick-start"
}
]
}
]
},
Expand Down Expand Up @@ -949,6 +966,19 @@
"to": "framework/preact/guides/does-this-replace-client-state"
}
]
},
{
"label": "octane",
"children": [
{
"label": "Suspense and Error Boundaries",
"to": "framework/octane/guides/suspense"
},
{
"label": "Server Rendering and Hydration",
"to": "framework/octane/guides/ssr"
}
]
}
]
},
Expand Down Expand Up @@ -1427,6 +1457,15 @@
"to": "framework/preact/reference/functions/HydrationBoundary"
}
]
},
{
"label": "octane",
"children": [
{
"label": "Octane Reference",
"to": "framework/octane/reference/index"
}
]
}
]
},
Expand Down
63 changes: 63 additions & 0 deletions docs/framework/octane/guides/ssr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
id: ssr
title: Server Rendering and Hydration
---

Octane Query supports both prefetched hydration and render-time Suspense on the
server. The binding re-exports Query Core's `dehydrate` and `hydrate` functions,
and `HydrationBoundary` accepts dehydrated queries, including pending queries
that resolve in a later stream chunk.

## Per-request clients

Create a new `QueryClient` for every server request. Never share one server
client between users.

```ts
import { QueryClient, dehydrate, hydrate } from '@tanstack/octane-query'

export async function loadQueryState() {
const queryClient = new QueryClient({
defaultOptions: {
queries: { staleTime: 60_000 },
},
})

await queryClient.prefetchQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})

return {
queryClient,
state: dehydrate(queryClient),
}
}

export function restoreQueryState(state: unknown) {
const queryClient = new QueryClient()
hydrate(queryClient, state)
return queryClient
}
```

Serialize the dehydrated state through the application's SSR pipeline, restore
it before the browser renders query consumers, and provide the browser client
with `QueryClientProvider`. Use `HydrationBoundary` when only a subtree owns a
dehydrated state payload.

The adapter owns Query's Octane binding and hydration behavior. Routing,
serialization, and response streaming remain responsibilities of the
application's TanStack Start or other SSR pipeline.

## Render-time Suspense

Suspense hooks can start a query during server rendering. Octane tracks each
thenable with the compiler-provided hook slot, so sequential suspense calls and
streamed continuations resume at the correct call site. Wrap those consumers in
an Octane Suspense boundary and let the SSR renderer stream the resolved
content.

For persisted browser caches, wrap consumers in `IsRestoringProvider` while the
restore is in progress. Query observers will avoid fetching until restoration
finishes.
43 changes: 43 additions & 0 deletions docs/framework/octane/guides/suspense.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
id: suspense
title: Suspense and Error Boundaries
---

Use `useSuspenseQuery`, `useSuspenseInfiniteQuery`, or
`useSuspenseQueries` when pending work should be handled by an Octane Suspense
boundary. These hooks integrate with Octane's `use(thenable)` behavior and
return results whose `data` is defined after the boundary resolves.

```tsrx
import { ErrorBoundary, Suspense } from 'octane'
import { useSuspenseQuery } from '@tanstack/octane-query'

function Profile() @{
const profile = useSuspenseQuery({
queryKey: ['profile'],
queryFn: fetchProfile,
})

<h1>{profile.data.name}</h1>
}

function Page() @{
<ErrorBoundary fallback={(error) => <p>{error.message}</p>}>
<Suspense fallback={<p>{'Loading...'}</p>}>
<Profile />
</Suspense>
</ErrorBoundary>
}
```

Octane's `@pending` and `@catch` directives can be used instead of the
component boundaries.

By default, a suspense query only throws an error when no cached data is
available. Set `throwOnError` on non-suspense queries or mutations when their
errors should reach the nearest error boundary.

Use `QueryErrorResetBoundary` or `useQueryErrorResetBoundary` to coordinate a
boundary retry with the Query cache. Reset the Query boundary when the Octane
error boundary retries so the failed query can fetch again instead of
immediately throwing the same error.
24 changes: 24 additions & 0 deletions docs/framework/octane/installation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
id: installation
title: Installation
---

Install the official adapter and Octane:

```bash
pnpm add @tanstack/octane-query octane
```

Or use the equivalent command for npm, Yarn, or Bun.

`octane` is a peer dependency. `@tanstack/query-core` is included by the
adapter and all of its public APIs are re-exported from
`@tanstack/octane-query`.

## Compiler requirement

The adapter publishes TSRX source so Octane can compile the correct client and
server output. Use Octane's standard Vite integration in the consuming app.
The package is not a precompiled Node.js runtime entry point.

The adapter requires Node.js 22 or newer for its package tooling.
52 changes: 52 additions & 0 deletions docs/framework/octane/overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
id: overview
title: Overview
---

`@tanstack/octane-query` is the official Octane adapter for TanStack Query. It
uses `@tanstack/query-core` unchanged and binds Query observers to Octane's
hooks, context, Suspense, and component lifecycle.

Most binding-level code can move from React Query by changing the import. The
adapter includes queries, infinite queries, parallel queries, mutations,
prefetch hooks, cache status hooks, Suspense hooks, error reset boundaries,
restoration state, and hydration. It also re-exports every Query Core API.

```tsrx
import {
QueryClient,
QueryClientProvider,
useQuery,
} from '@tanstack/octane-query'

const queryClient = new QueryClient()

function Todos() @{
const query = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})

@if (query.isPending) {
<span>{'Loading'}</span>
} @else if (query.isError) {
<span>{query.error.message}</span>
} @else {
<ul>{query.data as unknown}</ul>
}
}

function App() @{
<QueryClientProvider client={queryClient}>
<Todos />
</QueryClientProvider>
}
```

Octane hooks receive compiler-injected call-site slots. The adapter derives
stable sub-slots for the Query observer, subscription, and effects, so call the
hooks normally and let the Octane compiler transform them.

Start with [Installation](./installation.md), then [Quick Start](./quick-start.md).
See [Suspense and Error Boundaries](./guides/suspense.md) and [Server Rendering
and Hydration](./guides/ssr.md) for the framework-specific behavior.
60 changes: 60 additions & 0 deletions docs/framework/octane/quick-start.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
id: quick-start
title: Quick Start
---

Create one `QueryClient` for the browser application, provide it above query
consumers, and call the Octane hooks from component bodies.

```tsrx
import {
QueryClient,
QueryClientProvider,
useMutation,
useQuery,
useQueryClient,
} from '@tanstack/octane-query'
import { addTodo, getTodos } from './api'

const queryClient = new QueryClient()

function Todos() @{
const client = useQueryClient()
const todos = useQuery({
queryKey: ['todos'],
queryFn: getTodos,
})
const addTodoMutation = useMutation({
mutationFn: addTodo,
onSuccess: async () => {
await client.invalidateQueries({ queryKey: ['todos'] })
},
})

@if (todos.isPending) {
<p>{'Loading...'}</p>
} @else if (todos.isError) {
<p>{todos.error.message}</p>
} @else {
<ul>
{todos.data.map((todo) => <li>{todo.title}</li>)}
</ul>
<button
disabled={addTodoMutation.isPending}
onClick={() => addTodoMutation.mutate({ title: 'Read the Query docs' })}
>
{'Add todo'}
</button>
}
}

function App() @{
<QueryClientProvider client={queryClient}>
<Todos />
</QueryClientProvider>
}
```

Query option objects, keys, invalidation, retries, caching, and mutations use
the same TanStack Query contracts as the other framework adapters. The Octane
adapter changes the rendering and subscription layer, not Query Core.
Loading
Loading