Shadcn Hooks

useTitle

A hook to reactively manage document.title

Loading...

Installation

npx shadcn@latest add @shadcnhooks/use-title
pnpm dlx shadcn@latest add @shadcnhooks/use-title
yarn dlx shadcn@latest add @shadcnhooks/use-title
bun x shadcn@latest add @shadcnhooks/use-title

Copy and paste the following code into your project.

use-isomorphic-layout-effect.ts
import { useEffect, useLayoutEffect } from 'react'
import { isBrowser } from '@/registry/lib/is-browser'

/**
 * Custom hook that uses either `useLayoutEffect` or `useEffect` based on the environment (client-side or server-side).
 * @param {Function} effect - The effect function to be executed.
 * @param {Array<any>} [dependencies] - An array of dependencies for the effect (optional).
 * @public
 * @see [Documentation](https://usehooks-ts.com/react-hook/use-isomorphic-layout-effect)
 * @example
 * ```tsx
 * useIsomorphicLayoutEffect(() => {
 *   // Code to be executed during the layout phase on the client side
 * }, [dependency1, dependency2]);
 * ```
 */
export const useIsomorphicLayoutEffect = isBrowser ? useLayoutEffect : useEffect

use-title.ts
import { useEffect, useMemo, useRef, useState } from 'react'
import { useIsomorphicLayoutEffect } from '@/registry/hooks/use-isomorphic-layout-effect'
import type { Dispatch, SetStateAction } from 'react'

export interface UseTitleOptions {
  /**
   * Observe external changes to `document.title`.
   * @default false
   */
  observe?: boolean
  /**
   * Optional template for the final browser title.
   * Use `%s` as the placeholder when providing a string template.
   */
  titleTemplate?: string | ((title: string) => string)
}

function getDocumentTitle(): string {
  if (typeof document === 'undefined') {
    return ''
  }

  return document.title
}

function parseTemplate(
  titleTemplate?: string | ((title: string) => string),
): (title: string) => string {
  if (typeof titleTemplate === 'function') {
    return titleTemplate
  }

  if (typeof titleTemplate === 'string' && titleTemplate.includes('%s')) {
    return (title: string) => titleTemplate.replaceAll('%s', title)
  }

  return (title: string) => title
}

/**
 * Reactively read and update `document.title`.
 *
 * @param newTitle Optional initial/controlled title value.
 * @param options Hook options.
 */
export function useTitle(
  newTitle?: string | null,
  options: UseTitleOptions = {},
): readonly [string, Dispatch<SetStateAction<string>>] {
  const { observe = false, titleTemplate } = options
  const formatTitle = useMemo(
    () => parseTemplate(titleTemplate),
    [titleTemplate],
  )
  const [title, setTitle] = useState<string>(
    () => newTitle ?? getDocumentTitle(),
  )
  const lastInternalTitleRef = useRef<string | null>(null)

  useEffect(() => {
    if (newTitle == null) {
      return
    }

    setTitle((currentTitle) =>
      currentTitle === newTitle ? currentTitle : newTitle,
    )
  }, [newTitle])

  useIsomorphicLayoutEffect(() => {
    if (typeof document === 'undefined') {
      return
    }

    const nextDocumentTitle = formatTitle(title)
    lastInternalTitleRef.current = nextDocumentTitle
    if (document.title !== nextDocumentTitle) {
      document.title = nextDocumentTitle
    }
  }, [formatTitle, title])

  useEffect(() => {
    if (!observe || typeof document === 'undefined') {
      return
    }

    if (typeof MutationObserver === 'undefined') {
      return
    }

    const observerTarget = document.head ?? document.documentElement
    if (!observerTarget) {
      return
    }

    const observer = new MutationObserver(() => {
      const nextTitle = document.title
      if (lastInternalTitleRef.current === nextTitle) {
        return
      }

      setTitle((currentTitle) => {
        return currentTitle === nextTitle ? currentTitle : nextTitle
      })
    })

    observer.observe(observerTarget, {
      childList: true,
      subtree: true,
      characterData: true,
    })

    return () => {
      observer.disconnect()
    }
  }, [observe])

  return [title, setTitle] as const
}

API

export interface UseTitleOptions {
  /**
   * Observe external changes to `document.title`.
   * @default false
   */
  observe?: boolean
  /**
   * Optional template for the final browser title.
   * Use `%s` as the placeholder when providing a string template.
   */
  titleTemplate?: string | ((title: string) => string)
}

/**
 * Reactively read and update `document.title`.
 *
 * @param newTitle Optional initial/controlled title value.
 * @param options Hook options.
 */
export function useTitle(
  newTitle?: string | null,
  options?: UseTitleOptions,
): readonly [string, Dispatch<SetStateAction<string>>]

Credits

Last updated on

On this page