Skip to the solution
HardFrontend6 min read

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

311 views

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.

  • react
  • react-19
  • metadata
  • seo

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

6 min read · 9 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactHoisting title, meta and link — and watching two titles both land
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>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 107 of 119 in React.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All React.js questions
Up nextHard

What causes hydration mismatches, and how do you handle them?

Continue