useTitle
A hook to reactively manage document.title
Loading...
Installation
npx shadcn@latest add @shadcnhooks/use-titlepnpm dlx shadcn@latest add @shadcnhooks/use-titleyarn dlx shadcn@latest add @shadcnhooks/use-titlebun x shadcn@latest add @shadcnhooks/use-titleCopy and paste the following code into your project.
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 : useEffectimport { 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