---
title: "Vue Reactivity and Async Context"
description: "Use refs and computed with useHead() in Nuxt. Preserve Nuxt context across async work or update reactive state from callbacks."
canonical_url: "https://unhead.unjs.io/docs/nuxt/head/guides/core-concepts/reactivity"
last_updated: "2026-07-29T15:04:14.892Z"
---

Pass refs and computed values directly to `useHead()` or `useSeoMeta()`; Vue tracks them for you. Capture `injectHead()` before asynchronous work when a later call may run outside Nuxt context.

## Nuxt Integration

[`useHead()`](/docs/head/api/composables/use-head) accepts refs, computed refs, and getter functions. The Vue adapter resolves them in a `watchEffect()` and patches the same entry when a dependency changes.

Nuxt supplies the head instance through its application context, so its auto-imported composables do not need a `head` argument.

### Component Lifecycle

The adapter also follows Vue's component lifecycle:

- When a component is unmounted, any head entries created by that component are automatically removed
- When a component is deactivated with keep-alive, its entries will be deactivated
- When a component is activated with keep-alive, its entries will be reactivated

### Client and Server Reactivity

Reactivity behaves differently depending on the rendering context:

- **Server-Side Rendering (SSR)**: Values are resolved only when the tags are being rendered, usually after the app has finished rendering.
- **Client-Side Rendering (CSR)**: Any ref changes trigger a DOM update, making the head tags reactive after hydration.

## Reactive Values

Unhead works with all Vue reactive primitives:

```ts
import { useHead } from '#imports'
import { computed, ref } from 'vue'

// Create reactive state
const title = ref('My Site')
const description = ref('Welcome to my website')
const product = ref({ name: 'Widget' })

// Use reactive values in head tags
useHead({
  // Direct ref
  title,
  meta: [
    // Computed getter (recommended for derived values)
    { name: 'description', content: () => description.value },
    // Using refs directly in objects
    { property: 'og:title', content: title }
  ],
  // Computed ref
  link: [computed(() => ({
    rel: 'canonical',
    href: `https://example.com/products/${product.value.name}`
  }))]
})
```

## Async Context

The `inject()` function keeps track of your Vue component instance, but after async operations within lifecycle hooks or nested functions, Vue can lose track of this context.

```vue
<script setup lang="ts">
import { useHead } from '#imports'
import { onMounted } from 'vue'

onMounted(async () => {
  await someAsyncOperation()
  // This will throw an error
  useHead({
    title: 'My Title'
  })
})
</script>
```

When trying to inject once Vue has lost the context, you'll receive an error from Unhead:

<warning>

useHead() was called without provide context.

</warning>

Choose the pattern that matches where the asynchronous work runs.

### Top-Level Await

Vue's `<script setup>` compiler [preserves component context across top-level `await`](https://vuejs.org/api/sfc-script-setup.html#top-level-await) through a compile-time transform.

At the top level of script setup, context is automatically preserved:

```vue
<script setup lang="ts">
import { useHead } from '#imports'

// The compiler transforms this to preserve context
await someAsyncOperation()
useHead({
  title: 'My Title'
})
</script>
```

Outside compiler-transformed code, Nuxt context can be unavailable after complex asynchronous work. Nuxt documents this limitation alongside [`runWithContext()`](https://nuxt.com/docs/4.x/api/composables/use-nuxt-app#runwithcontext), which should be a fallback rather than the default pattern.

### Create Entries Before Awaiting

Create entries synchronously while Nuxt context is active, then patch them from asynchronous callbacks. This keeps lifecycle cleanup attached to the component scope:

```vue
<script setup lang="ts">
import { useHead } from '#imports'
import { onMounted } from 'vue'

const pageHead = useHead({
  title: 'My Site'
})
const analyticsHead = useHead({})

async function updatePageHead(id: string) {
  const data = await fetchPage(id)

  pageHead.patch({
    title: data.title,
    meta: [
      {
        name: 'description',
        content: data.description
      }
    ]
  })
}

onMounted(async () => {
  const analyticsUrl = await loadAnalyticsUrl()

  analyticsHead.patch({
    script: [
      {
        // Only use URLs from an allowlisted, trusted origin.
        src: analyticsUrl
      }
    ]
  })
})
</script>
```

Do not place strings from APIs or users in a script's `innerHTML` or `textContent`; both execute as JavaScript. Vue's [security guide](https://vuejs.org/guide/best-practices/security#potential-dangers) recommends avoiding untrusted JavaScript and URLs.

### Update Reactive State After Async Work

Define the head entry during setup and update reactive state when the request completes:

```vue
<script setup lang="ts">
import { useHead } from '#imports'
import { computed, ref } from 'vue'

// Initialize your reactive state
const page = ref({
  title: 'Loading...',
  description: '',
  image: '/placeholder.jpg'
})

// Define head once with computed properties
useHead({
  // Title will automatically update when page.value.title changes
  title: computed(() => page.value.title),
  meta: [
    {
      name: 'description',
      content: computed(() => page.value.description)
    },
    {
      property: 'og:image',
      content: computed(() => page.value.image)
    }
  ]
})

// Async operations update the reactive state
async function loadPage(id: string) {
  const data = await fetchPage(id)
  // Head updates automatically when we update the ref
  page.value = {
    title: data.title,
    description: data.description,
    image: data.image
  }
}

// Works great with watchers too
watch(route, async () => {
  await loadPage(route.params.id)
})
</script>
```

#### Pinia

The same pattern works with a Pinia store:

```vue
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { usePageStore } from '@/stores/page'

const store = usePageStore()
// Destructure with storeToRefs to maintain reactivity
const { title, description } = storeToRefs(store)

useHead({
  title, // Reactive store state automatically works
  meta: [
    {
      name: 'description',
      content: description
    }
  ]
})

// Now your store actions can update the head
await store.fetchPage(id)
</script>
```

## Common Patterns

### Refs and Computed Getters

Prefer direct refs and computed getters:

```ts
// ✅ Good approach
const title = ref('Product Page')
const product = ref({ name: 'Widget' })

useHead({
  // Direct ref
  title,
  // Computed getter
  meta: [
    { property: 'og:title', content: () => `${product.value.name} - ${title.value}` }
  ]
})
```

### Avoid Creating Entries in Watchers

Avoid `useHead()` calls in watchers, as this creates new entries on each update:

```ts
// ❌ Bad approach: Creates multiple entries
watch(title, (newTitle) => {
  useHead({
    title: newTitle
  })
})

// ✅ Good approach: Updates existing entry
useHead({
  title // ref value updates automatically
})
```

### KeepAlive

Vue's `<KeepAlive>` preserves component state across deactivation. Unhead deactivates and restores that component's entries at the same time:

```vue
// Using keep-alive allows Unhead to optimize head updates
<KeepAlive>
  <component :is="currentView" />
</KeepAlive>
```

## Examples

### Dynamic SEO Metadata

```ts
import { useRoute } from 'vue-router'

export default {
  setup() {
    const route = useRoute()
    const product = ref(null)

    // Fetch data based on route
    fetchProduct(route.params.id).then((data) => {
      product.value = data
    })

    // SEO tags update automatically when product data is loaded
    useSeoMeta({
      title: () => product.value?.name || 'Loading...',
      description: () => product.value?.description || '',
      ogImage: () => product.value?.image || '/default.jpg',
    })
  }
}
```

### Multiple Entries

You can call `useHead()` in several components. Unhead resolves their entries together:

```ts
// BaseLayout.vue
useHead({
  titleTemplate: '%s | My Site',
  meta: [
    { name: 'theme-color', content: '#ff0000' }
  ]
})

// ProductPage.vue
useHead({
  title: () => product.value?.name || '',
  meta: [
    { name: 'description', content: () => product.value?.description || '' }
  ]
})
```

## Nuxt Async Context

Nuxt restores composable context across awaited code in transformed contexts such as `<script setup>`, `defineNuxtPlugin()`, and `defineNuxtRouteMiddleware()`. An arbitrary callback can still run outside that context. Define the head entry before the callback and update reactive state, or capture the head instance before awaiting and pass it in the composable options.

## Adapter Internals

Nuxt exposes the Vue adapter through its application context. The adapter unwraps refs and computed values in `watchEffect()` and registers activation and cleanup hooks on the current component. Vue's [`<script setup>` API](https://vuejs.org/api/sfc-script-setup.html#top-level-await) documents the top-level await transform used in the async examples above.
