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.
1. Track Signals in an Effect (Recommended)
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:
- In SSR, reactive values are resolved once when the page is rendered
- Changes to signals after initial render won't affect the server output
- 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 withpatch() - 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)
}
}