Index of every page on this site: llms.txt
Angular
Core Concepts

Reactivity in Angular

Create one entry with useHead(), read signals inside an Angular effect(), and patch that entry when they change. Use useUnhead() only when you need the underlying instance.

Angular Integration

The Angular adapter does not subscribe to signal getters inside a head object. Angular effects track signals read during execution, so read each signal in an effect and pass its current value to patch(). The adapter ties the resulting entry to the component lifecycle.

Track Signals in an Effect

Create the entry once and patch it from an effect:

import { Component, effect, signal } from '@angular/core'
import { useHead } from '@unhead/angular'

@Component({
  selector: 'app-counter',
  template: `
    <button (click)="incrementCounter()">Count: {{ counter() }}</button>
  `
})
export class CounterComponent {
  counter = signal(0)
  head = useHead()

  constructor() {
    effect(() => {
      this.head.patch({
        title: `Counter: ${this.counter()}`
      })
    })
  }

  incrementCounter() {
    this.counter.update(value => value + 1)
  }
}

The effect tracks counter() and patches the same head entry whenever the signal changes.

Patch an Existing Entry

Store the reference returned by useHead() when other methods also need to patch the entry:

import { Component, effect, signal } from '@angular/core'
import { useHead } from '@unhead/angular'

@Component({
  // ...
})
export class MyComponent {
  pageTitle = signal('Initial Title')
  head = useHead()

  constructor() {
    effect(() => {
      this.head.patch({ title: this.pageTitle() })
    })
  }

  updateTitle(newTitle: string) {
    this.pageTitle.set(newTitle)
  }

  // Imperative updates can patch the same entry.
  updateHeadContent() {
    this.head.patch({
      meta: [
        { name: 'description', content: 'New description' }
      ]
    })
  }
}

Reactivity Patterns

Choose between an effect and an imperative patch based on where the state changes.

import { Component, effect, signal } from '@angular/core'
import { useHead } from '@unhead/angular'

@Component({
  // ...
})
export class MyComponent {
  description = signal('Page description')
  head = useHead()

  constructor() {
    effect(() => {
      this.head.patch({
        meta: [
          { name: 'description', content: this.description() }
        ]
      })
    })
  }
}

2. Patch Imperatively

import { Component, signal } from '@angular/core'
import { useHead } from '@unhead/angular'

@Component({
  // ...
})
export class MyComponent {
  title = signal('Page Title')
  head = useHead()

  setTitle(title: string) {
    this.title.set(title)
    this.head.patch({ title })
  }
}

This is useful when every state change already goes through one method. Prefer an effect when several code paths can update the signal.

Server-Side Rendering

SSR resolves the entry for one request:

  1. In SSR, reactive values are resolved once when the page is rendered
  2. Changes to signals after initial render won't affect the server output
  3. The client will hydrate and take over reactivity after loading

To set initial SSR values, use the provideServerHead provider:

// app.config.server.ts
import { provideServerHead } from '@unhead/angular/server'

const serverConfig = {
  providers: [
    provideServerHead({
      init: [
        {
          htmlAttrs: {
            lang: 'en',
          },
          title: 'Server Default Title',
          meta: [
            { name: 'description', content: 'Server default description' }
          ]
        }
      ]
    }),
  ]
}

Practical Guidelines

DO

  • Create the head entry once: head = useHead()
  • Read signals inside effect() and update the entry with patch()
  • Patch imperatively when an existing method owns the state change
  • Set default values in server configuration

DON'T

  • Call useHead() inside an effect, which creates a new entry on each run
  • Expect getter functions in a head entry to subscribe to Angular signals
  • Forget to read the signal inside the effect that patches the entry

Cleanup

The adapter registers entry disposal with Angular's DestroyRef, whose callbacks run when the owning component, directive, or injector is destroyed.

Full Example

This example patches head attributes and SEO metadata from the same effect:

import { Component, computed, effect, signal } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { useHead, useSeoMeta } from '@unhead/angular'

@Component({
  selector: 'app-page',
  imports: [FormsModule],
  template: `
    <h1>{{ pageTitle() }}</h1>
    <input [ngModel]="pageTitle()" (ngModelChange)="setPageTitle($event)" />
    <button (click)="toggleDarkMode()">
      Toggle {{ isDarkMode() ? 'Light' : 'Dark' }} Mode
    </button>
  `,
})
export class PageComponent {
  pageTitle = signal('My Reactive Page')
  isDarkMode = signal(false)

  // Computed values work well with reactivity
  bodyClass = computed(() => this.isDarkMode() ? 'dark-theme' : 'light-theme')

  head = useHead()
  seo = useSeoMeta()

  constructor() {
    effect(() => {
      this.head.patch({
        title: this.pageTitle(),
        bodyAttrs: {
          class: this.bodyClass()
        }
      })
      this.seo.patch({
        description: `${this.pageTitle()} - Learn more about it!`
      })
    })
  }

  setPageTitle(newTitle: string) {
    this.pageTitle.set(newTitle)
  }

  toggleDarkMode() {
    this.isDarkMode.update(current => !current)
  }
}
Did this page help you?