Skip to content

SSR Action Queries

useActionQuery wraps Nuxt's useAsyncData to provide SSR-capable GET action queries with caching and reactive re-fetching.

Working example

See SSR queries in the example /queries page -- server-rendered data with reactive search.

Basic Usage

vue
<script setup lang="ts">
import { listTodos } from '#actions'

const { data, pending, refresh } = useActionQuery(listTodos)
</script>

<template>
  <div v-if="pending">Loading...</div>
  <ul v-else>
    <li v-for="todo in data" :key="todo.id">{{ todo.title }}</li>
  </ul>
</template>

Reactive Input

When input is a ref or getter, the query re-fetches automatically when it changes:

vue
<script setup lang="ts">
import { searchTodos } from '#actions'

const query = ref('')
const { data, pending } = useActionQuery(searchTodos, () => ({ q: query.value }))
</script>

<template>
  <input v-model="query" placeholder="Search...">
  <ul>
    <li v-for="todo in data" :key="todo.id">{{ todo.title }}</li>
  </ul>
</template>

Options

OptionTypeDefaultDescription
serverbooleantrueFetch on server during SSR
lazybooleanfalseDon't block navigation
immediatebooleantrueExecute immediately
default() => T-Default value factory
refetchIntervalnumber | false-Auto-refetch interval in ms
refetchOnFocusbooleanfalseRefetch when tab regains focus
refetchOnReconnectbooleanfalseRefetch when network reconnects
enabledboolean | Ref<boolean>trueConditionally enable/disable
transform(data: T) => T-Transform response data

Lazy Loading

ts
const { data, pending } = useActionQuery(listTodos, undefined, {
  lazy: true, // Don't block navigation
})

Default Values

ts
const { data } = useActionQuery(listTodos, undefined, {
  default: () => [], // Start with empty array
})

Polling

ts
const { data } = useActionQuery(listTodos, undefined, {
  refetchInterval: 5000, // Refetch every 5 seconds
})

Refetch on Focus

ts
const { data } = useActionQuery(listTodos, undefined, {
  refetchOnFocus: true, // Refetch when tab becomes visible
})

Refetch on Reconnect

ts
const { data } = useActionQuery(listTodos, undefined, {
  refetchOnReconnect: true, // Refetch when network comes back
})

Conditional Fetching

ts
const userId = ref<number | null>(null)
const isReady = computed(() => userId.value !== null)

const { data } = useActionQuery(
  getUserProfile,
  () => ({ id: userId.value! }),
  { enabled: isReady },
)

Transform Response

ts
const { data } = useActionQuery(listTodos, undefined, {
  transform: (todos) => todos.sort((a, b) => b.id - a.id),
})

Return Value

PropertyTypeDescription
dataComputedRef<T | null>Unwrapped success data
errorComputedRef<ActionError | null>Error if failed
statusRef<AsyncDataStatus>Nuxt async data status
pendingRef<boolean>Whether a request is in progress
refresh() => Promise<void>Re-fetch the data
clear() => voidClear the cached data

How It Works

  1. SSR: Data is fetched on the server and hydrated on the client
  2. Caching: Requests with the same key are deduplicated
  3. Reactive: When input changes, the query re-fetches automatically
  4. Unwrapping: ActionResult<T> is automatically unwrapped — data gives you T directly

Server Action Setup

Define a GET action on the server:

ts
// server/actions/list-todos.get.ts
import { z } from 'zod'

export default defineAction({
  input: z.object({
    q: z.string().optional(),
  }),
  handler: async ({ input }) => {
    return await db.todo.findMany({
      where: input.q ? { title: { contains: input.q } } : undefined,
    })
  },
})

Next Steps

For cursor-based pagination and infinite scroll, see the Infinite Queries guide.

Released under the MIT License.