How does React 19 handle document metadata like `<title>` and `<meta>`?
311 views
01
01
Understand the problem
Question presented to candidate:
"Before React 19 you needed a library to set the page title from a component. What changed?"
What a strong answer should cover:
React 19 natively hoists<title>, <meta> and <link> rendered anywhere in the tree into document.head. No portal, no library, no imperative document.title = ....
It works during SSR as well, so the tags are in the streamed HTML where crawlers and link-preview bots can read them without executing JavaScript.
The tags are removed from where you wrote them — they do not render inline in the component's container.
It does not deduplicate. Two components each rendering a <title> produce two <title> elements in the head. React hoists; it does not arbitrate.
So a framework's metadata API is still doing real work: merging, resolving precedence between layout and page, and templating titles.
Related but separate: the resource preloading APIs (preload, preinit), which React also manages and which are deduplicated.
The practical rule: use it for leaf-level, component-owned metadata; use the framework's metadata system for page-level titles and descriptions where precedence matters.
Clarifying questions expected:
"Are we in a framework with its own metadata API?" — then that usually wins for page-level tags.
"Does this need to be in the server-rendered HTML for crawlers?"
Code / implementation expected: Optional. It is a one-line demonstration; the dedup caveat is the substance.
reactreact-19metadataseo
02
02
Attempt it yourself
Sketch your approach before reading the solution — that's what interviews test.
Nudge consolestandby
Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.
03
03
Study the solution
Target Audience: Engineers preparing for React interviews — assumes JSX basics.
Difficulty: Hard
How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. Both behaviours in sections 3 and 4 were executed against React 19.2.8 — including t
Solution ready — 2 min read
Classified // press E to declassify
04
04
Explore the playground snippets
Hoisting title, meta and link — and watching two titles both land
import { useState, useEffect } from"react";
// Rendered deep inside the tree, nowhere near the head.functionArticleMeta({ article }) {
return (
<><title>{article.title}</title><metaname="description"content={article.summary} /><linkrel="canonical"href={article.url} /></>
);
}
// A second component that ALSO renders a title. React hoists both — it does// not arbitrate between them.functionWidgetMeta() {
return<title>Widget title — the second one</title>;
}
constARTICLES = [
{ id: 1, title: "Hoisting metadata in React 19", summary: "How title, meta and link tags move to the head.", url: "https://example.com/a" },
{ id: 2, title: "A completely different article", summary: "Switching articles changes the head live.", url: "https://example.com/b" },
];
exportdefaultfunctionApp() {
const [id, setId] = useState(1);
const [showSecond, setShowSecond] = useState(false);
const [head, setHead] = useState(null);
const article = ARTICLES.find((a) => a.id === id);
// Read the head back after each commit, so the effect of hoisting is visible.useEffect(() => {
const t = setTimeout(() => {
setHead({
title: document.title,
titleCount: document.head.querySelectorAll("title").length,
titles: [...document.head.querySelectorAll("title")].map((n) => n.textContent),
description: document.head.querySelector('meta[name="description"]')?.content ?? "(none)",
canonical: document.head.querySelector('link[rel="canonical"]')?.href ?? "(none)",
});
}, 30);
return() =>clearTimeout(t);
}, [id, showSecond]);
return (
<divstyle={{padding:24, fontFamily: "system-ui", lineHeight:1.6, maxWidth:560 }}><divstyle={{display: "flex", gap:8, marginBottom:12, flexWrap: "wrap" }}>
{ARTICLES.map((a) => (
<buttonkey={a.id}onClick={() => setId(a.id)} style={{ fontWeight: a.id === id ? "bold" : "normal" }}>
article {a.id}
</button>
))}
<buttononClick={() => setShowSecond((s) => !s)}>
{showSecond ? "remove" : "add"} a SECOND component with a title
</button></div><articlestyle={{border: "1pxsolid #ddd", borderRadius:8, padding:12 }}>
{/* These three tags are written HERE and end up in document.head. */}
<ArticleMetaarticle={article} />
{showSecond && <WidgetMeta />}
<h3style={{margin: "004px" }}>{article.title}</h3><pstyle={{fontSize:13, color: "#555", margin:0 }}>{article.summary}</p></article><prestyle={{background: "#f6f6f8", padding:10, borderRadius:8, fontSize:12, marginTop:12, minHeight:120, whiteSpace: "pre-wrap" }}>
{head ? [
"document.title: " + JSON.stringify(head.title),
"<title> in head: " + head.titleCount,
"their contents: " + JSON.stringify(head.titles),
"description: " + JSON.stringify(head.description),
"canonical: " + JSON.stringify(head.canonical),
].join("\n") : "reading the head…"}
</pre><pstyle={{fontSize:13, color: "#666" }}>
Switch articles: the head updates live, and none of the tags render
inside the bordered box. Now add the second component — you get{" "}
<strong>two</strong> title elements, not one. React hoists; it does not
decide which should win. That is exactly the job a framework metadata
API is still doing.
</p></div>
);
}