Document Metadata - Render title, meta, and link tags anywhere in the component tree
Recipe
function BlogPost({ post }: { post: { title: string; description: string; author: string } }) {
return (
<article>
{/* These are hoisted to <head> automatically */}
<title>{post.title}</title>
<meta name="description" content={post.description} />
<meta name="author" content={post.author} />
<link rel="canonical" href={`https://example.com/blog/${post.title}`} />
<h1>{post.title}</h1>
<p>{post.description}</p>
</article>
);
}When to reach for this: Use built-in metadata whenever a component needs to set the page title, description, or other head tags. This eliminates the need for react-helmet or manual document.title effects.
Working Example
// A multi-page app where each route sets its own metadata
function ProductPage({ product }: {
product: {
name: string;
description: string;
price: number;
image: string;
category: string;
};
}) {
return (
<main>
{/* Page title */}
<title>{product.name} | My Store</title>
{/* SEO meta tags */}
<meta name="description" content={product.description} />
<meta name="keywords" content={`${product.category}, buy, shop`} />
{/* Open Graph tags */}
<meta property="og:title" content={product.name} />
<meta property="og:description" content={product.description} />
<meta property="og:image" content={product.image} />
<meta property="og:type" content="product" />
{/* Twitter card */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={product.name} />
<meta name="twitter:image" content={product.image} />
{/* Preload the product image */}
<link rel="preload" href={product.image} as="image" />
{/* Stylesheet for this page */}
<link rel="stylesheet" href="/styles/product.css" precedence="default" />
{/* Page content */}
<div className="max-w-4xl mx-auto p-6">
<img src={product.image} alt={product.name} />
<h1 className="text-3xl font-bold">{product.name}</h1>
<p className="text-xl">${product.price.toFixed(2)}</p>
<p>{product.description}</p>
</div>
</main>
);
}
function SettingsPage() {
return (
<div>
<title>Settings | My Store</title>
<meta name="robots" content="noindex" />
<h1>Account Settings</h1>
{/* ... */}
</div>
);
}
export { ProductPage, SettingsPage };What this demonstrates:
<title>rendered inline, automatically hoisted to<head>- SEO meta tags (description, keywords) set per-page
- Open Graph and Twitter Card tags for social sharing
<link rel="preload">for performance optimization<link rel="stylesheet">withprecedencefor CSS ordering- Different pages setting completely different metadata
Deep Dive
How It Works
- React 19 recognizes
<title>,<meta>, and<link>elements when rendered inside a component and hoists them to the document<head>. - This works in all rendering modes: client-only SPAs, streaming SSR, and Server Components.
- During SSR, metadata tags are included in the initial HTML
<head>. During client-side navigation, React updates the<head>by adding, removing, or updating the hoisted elements. <title>tags are deduplicated -- only the last rendered<title>wins if multiple components render one.<meta>tags are deduplicated by thenameorpropertyattribute. Two<meta name="description">tags from different components will result in only the most recent one.<link rel="stylesheet">tags support aprecedenceattribute that controls insertion order. Stylesheets with the sameprecedenceare grouped together. React also deduplicates stylesheet links byhref.- Other
<link>types (preload, icon, canonical, etc.) are hoisted but not deduplicated by default.
Variations
Dynamic title with live data:
function ChatRoom({ roomName, unreadCount }: { roomName: string; unreadCount: number }) {
const titlePrefix = unreadCount > 0 ? `(${unreadCount}) ` : "";
return (
<div>
<title>{titlePrefix}{roomName} | Chat</title>
<h1>{roomName}</h1>
{/* chat UI */}
</div>
);
}Nested components adding metadata:
function Layout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
{/* Base metadata */}
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
{children}
</body>
</html>
);
}
function AboutPage() {
return (
<Layout>
{/* Page-specific metadata merges with layout metadata */}
<title>About Us</title>
<meta name="description" content="Learn about our company" />
<h1>About Us</h1>
</Layout>
);
}Stylesheet precedence ordering:
function App() {
return (
<>
{/* "default" precedence loads first */}
<link rel="stylesheet" href="/base.css" precedence="default" />
{/* "high" precedence loads after "default" */}
<link rel="stylesheet" href="/theme.css" precedence="high" />
</>
);
}
function Widget() {
return (
<>
{/* Same precedence groups together with other "default" stylesheets */}
<link rel="stylesheet" href="/widget.css" precedence="default" />
<div>Widget content</div>
</>
);
}TypeScript Notes
- The
<title>,<meta>, and<link>JSX elements use their standard HTML attribute types fromReact.JSX.IntrinsicElements. - The
precedenceattribute on<link>is a React-specific extension typed asstring | undefined. - No special imports are needed -- these are standard JSX elements.
Gotchas
- Multiple
<title>tags -- Only the last rendered<title>applies. If two sibling components both render<title>, the result depends on render order. Fix: Set<title>in only one component per route, typically the page-level component. - Framework metadata APIs may conflict -- Next.js has its own
metadataexport andgenerateMetadata. Using both that and inline<title>tags can cause duplicates. Fix: Choose one approach per project. For Next.js apps, prefer the framework'smetadataAPI. - Meta tags outside
<head>in SSR HTML -- React hoists tags during rendering, but if you inspect the raw HTML response, they appear in<head>. If hydration mismatches occur, check for server/client differences. Fix: Ensure metadata values are deterministic (noDate.now()orMath.random()). <link rel="stylesheet">withoutprecedence-- If you omitprecedence, the stylesheet link is treated as a regular link and is not deduplicated or ordered by React. Fix: Always addprecedenceto stylesheet links for React-managed ordering.- No support for
<script>hoisting -- React 19 does not hoist<script>tags to<head>. Fix: Usepreinitfromreact-domfor scripts, or place script tags in your HTML template.
Alternatives
| Approach | When to choose |
|---|---|
| Built-in metadata (React 19) | Default choice for new React 19 projects |
Next.js metadata export | Next.js App Router projects with static or dynamic metadata |
react-helmet / react-helmet-async | React 18 or older projects |
Manual document.title in useEffect | Quick hack for client-only title changes |
HTML <head> in template | Static metadata that never changes |
FAQs
How does React 19 handle title, meta, and link tags rendered inside components?
- React recognizes these elements and hoists them to the document
<head>automatically - This works in all rendering modes: client-only SPAs, streaming SSR, and Server Components
- During client-side navigation, React updates the
<head>by adding, removing, or updating hoisted elements
What happens if multiple components render a title tag?
- Only the last rendered
<title>applies -- React deduplicates them - If two sibling components both render
<title>, the result depends on render order - Set
<title>in only one component per route, typically the page-level component
How are meta tags deduplicated?
<meta>tags are deduplicated by thenameorpropertyattribute- Two
<meta name="description">tags from different components result in only the most recent one - Other
<link>types (preload, icon, canonical) are hoisted but not deduplicated by default
What does the precedence attribute do on stylesheet link tags?
precedencecontrols insertion order of stylesheets in the<head>- Stylesheets with the same
precedencevalue are grouped together - React also deduplicates stylesheet links by
hrefand suspends rendering until the stylesheet loads
How do you set Open Graph and Twitter Card tags with React 19 metadata?
function Page({ title, description, image }) {
return (
<>
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
{/* page content */}
</>
);
}Can you dynamically update the title with live data (e.g., unread count)?
function ChatRoom({ roomName, unreadCount }) {
const prefix = unreadCount > 0 ? `(${unreadCount}) ` : "";
return (
<>
<title>{prefix}{roomName} | Chat</title>
{/* chat UI */}
</>
);
}The title updates reactively as unreadCount changes.
Can nested components from layouts and pages both contribute metadata?
- Yes. A layout can set base metadata (
<meta charSet>,<link rel="icon">) and child pages add page-specific tags - Tags are merged -- page-specific
<title>overrides the layout's if both render one <meta>deduplication byname/propertyensures no duplicates
Gotcha: Should I use React 19 inline metadata or Next.js metadata export?
- Next.js has its own
metadataexport andgenerateMetadatawhich may conflict with inline tags - Using both can cause duplicate tags in
<head> - Choose one approach per project; for Next.js apps, prefer the framework's
metadataAPI
Gotcha: Does React 19 hoist script tags to the head?
- No. React 19 does not hoist
<script>tags to<head> - Use
preinitfromreact-domfor scripts, or place script tags in your HTML template - This only applies to
<title>,<meta>, and<link>elements
What happens if a stylesheet link tag is missing the precedence attribute?
- Without
precedence, the stylesheet is treated as a regular link and is not deduplicated or ordered by React - React also will not suspend rendering for it, potentially causing a flash of unstyled content
- Always add
precedenceto stylesheet links you want React to manage
How is the precedence attribute typed in TypeScript?
- The
precedenceattribute on<link>is a React-specific extension typed asstring | undefined <title>,<meta>, and<link>use their standard HTML attribute types fromReact.JSX.IntrinsicElements- No special imports are needed -- these are standard JSX elements
How do you ensure metadata values do not cause hydration mismatches?
- Ensure metadata values are deterministic -- avoid
Date.now(),Math.random(), or other non-deterministic values - Server and client must produce the same metadata content during hydration
- Use stable values from props, params, or database queries
Related
- Asset Loading -- Preloading resources with
preloadandpreinit - Server Components -- Metadata in server-rendered pages
- Overview -- Full list of React 19 features