Skip to solution
hardFrontend

How does React 19 handle document metadata like `<title>` and `<meta>`?

311 views
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

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

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

Explore the playground snippets

Hoisting title, meta and link — and watching two titles both land
Run Playground
import { useState, useEffect } from "react";

// Rendered deep inside the tree, nowhere near the head.
function ArticleMeta({ article }) {
  return (
    <>
      <title>{article.title}</title>
      <meta name="description" content={article.summary} />
      <link rel="canonical" href={article.url} />
    </>
  );
}

// A second component that ALSO renders a title. React hoists both — it does
// not arbitrate between them.
function WidgetMeta() {
  return <title>Widget title — the second one</title>;
}

const ARTICLES = [
  { 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" },
];

export default function App() {
  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 (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6, maxWidth: 560 }}>
      <div style={{ display: "flex", gap: 8, marginBottom: 12, flexWrap: "wrap" }}>
        {ARTICLES.map((a) => (
          <button key={a.id} onClick={() => setId(a.id)} style={{ fontWeight: a.id === id ? "bold" : "normal" }}>
            article {a.id}
          </button>
        ))}
        <button onClick={() => setShowSecond((s) => !s)}>
          {showSecond ? "remove" : "add"} a SECOND component with a title
        </button>
      </div>

      <article style={{ border: "1px solid #ddd", borderRadius: 8, padding: 12 }}>
        {/* These three tags are written HERE and end up in document.head. */}
        <ArticleMeta article={article} />
        {showSecond && <WidgetMeta />}

        <h3 style={{ margin: "0 0 4px" }}>{article.title}</h3>
        <p style={{ fontSize: 13, color: "#555", margin: 0 }}>{article.summary}</p>
      </article>

      <pre style={{ 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>

      <p style={{ 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>
  );
}
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 107 of 119 decoded in the React.js track. One more won't hurt.

Back to track