Metadata, stylesheets and resource hints
Render title and meta tags from any component, order stylesheets with precedence, load async scripts once, and preload files with React 19's react-dom APIs.
- 01Render a page's metadata from its componentPut title, meta and link tags in the component they belong to, and know when React moves them into the head and when it leaves them in place.
- 02Load stylesheets and scripts safelyOrder stylesheets with precedence, keep one copy of each, let Suspense wait for them, and render async scripts that run once.
- 03Tell the browser about files earlyChoose between prefetchDNS, preconnect, preload, preinit and their module versions, in the browser and in server-rendered HTML.
A page title that travels with its page
Every page of a shop needs a few tags that never show inside the page itself. The <title> is the text on the browser tab, in bookmarks and in search results. A <meta name="description"> is the summary a search engine may show under the link. Both belong in the document's <head>, the part of the HTML before <body> that describes the page.
The component that knows the right title is the product page: it has the product. In React 19, it can render the tags itself. The result is live. Click Black tea:
1import { useState } from "react";2import HeadView from "./HeadView.jsx";3 4const teas = {5 green: { name: "Green tea", summary: "Loose leaf green tea, 100 g" },6 black: { name: "Black tea", summary: "Strong breakfast black tea, 100 g" },7};8 9function ProductPage({ tea }) {10 return (11 <main>12 <title>{`${tea.name} | Tea Shop`}</title>13 <meta name="description" content={tea.summary} />14 <h1>{tea.name}</h1>15 <p>{tea.summary}</p>16 </main>17 );18}19 20export default function App() {21 const [id, setId] = useState("green");22 return (23 <>24 <button onClick={() => setId("green")}>Green tea</button>25 <button onClick={() => setId("black")}>Black tea</button>26 <ProductPage tea={teas[id]} />27 <HeadView />28 </>29 );30}ProductPage (lines 9 to 18) returns a <main> with four tags in it. Line 12 is the title, built as one string with a template literal. Line 13 is the description. Lines 14 and 15 are the heading and the summary you see on the page.
Under the buttons, the HeadView.jsx tab lists every tag added to the page's <head> since it loaded, and the value of document.title. It shows the <title> and the <meta> there, not inside <main>: React moved them. Click Black tea and both change, and document.title reads "Black tea | Tea Shop". Moving an element up to the head like this is called hoisting.
ProductPage returns all four tags together. React puts <title> and <meta> in the document's <head>, and <h1> and <p> in the page, inside <main>.You don't need to read HeadView.jsx to follow the lesson, but it's short. Line 4 remembers the tags index.html already had. readHead (lines 6 to 9) lists the others. Lines 16 to 22 use a MutationObserver to read the head again whenever it changes.
import { useEffect, useState } from "react";
// The tags index.html put in <head>, so the list shows only the ones added later.
const fromIndexHtml = new Set(document.head.children);
function readHead() {
const added = [...document.head.children].filter((tag) => !fromIndexHtml.has(tag));
return added.map((tag) => tag.outerHTML);
}
export default function HeadView({ log = false }) {
const [tags, setTags] = useState(readHead);
useEffect(() => {
setTags(readHead());
const observer = new MutationObserver((changes) => {
for (const change of changes)
for (const tag of change.addedNodes)
if (log && change.target === document.head) console.log("added to <head>:", tag.outerHTML);
setTags(readHead());
});
observer.observe(document.head, { childList: true, subtree: true, characterData: true });
return () => observer.disconnect();
}, [log]);
return (
<section style={{ borderTop: "1px solid gray", marginTop: 12 }}>
<p>Added to <head> since the page loaded:</p>
<pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-all", fontSize: 12 }}>
{tags.length ? tags.join("\n") : "(nothing)"}
</pre>
<p>document.title: {JSON.stringify(document.title)}</p>
</section>
);
}Before React 19, a component set the title with an Effect that wrote document.title, or with a library such as react-helmet:
function ProductPage({ tea }) {
useEffect(() => {
document.title = `${tea.name} | Tea Shop`;
}, [tea]);
return <h1>{tea.name}</h1>;
}Line 3 writes the title after each render where tea changed. That works in the browser, but Effects never run on the server, so a server-rendered page arrives with the wrong title. A description needs more code again: find the <meta>, then set its content. How to read the table: each row asks one question of both ways.
| Question | An Effect writes document.title | The component renders <title> |
|---|---|---|
| Is the title in the server's HTML? | No. Effects don't run on the server, so the HTML has the old title. | Yes. React writes it into the HTML's head. |
| When does the tab change? | After the page has painted, when the Effect runs. | In the same commit that shows the new page. |
| What about a description? | You find the meta tag yourself and set its content. | You render a meta tag, like any other element. |
| What happens when the component goes away? | The title stays as the Effect left it, unless you write a cleanup. | React removes the tag, and the head goes back to what it had. |
At a conference, speakers don't walk around the hall putting up signs. Each one brings a badge, and the organizer pins it on the board by the door, where visitors look. When a speaker leaves, the organizer takes the badge down again.
- In real life: Each speaker brings their own name badge
- In React: Each component renders its own <title> and <meta>
- In real life: The organizer pins it on the board by the door
- In React: React places the tag in <head>
- In real life: When the speaker leaves, their badge comes down
- In React: When the component unmounts, React removes its tags
- In real life: One cloakroom ticket per coat, however often you ask
- In React: One stylesheet per href, however many components render it
In React 19, any component can render <title>, <meta> and <link>, and React DOM places them in the document's <head>. Stylesheets with precedence and async scripts go there too, once each, in a safe order. The functions preload, preinit and friends from react-dom tell the browser about files before it would find them.
Title and meta tags from any component
<title> has one rule that trips everyone up: its children must be a single string. This search page looks fine in the code. Look at the title in the result:
1import { useState } from "react";2import HeadView from "./HeadView.jsx";3 4export default function App() {5 const [page, setPage] = useState(1);6 return (7 <>8 <title>Search results, page {page}</title>9 <button onClick={() => setPage(page + 1)}>Next page</button>10 <p>Showing page {page} of the tea search.</p>11 <HeadView />12 </>13 );14}HeadView shows <title></title> and document.title: "": an empty title. On line 8, JSX gives <title> two children, the text "Search results, page " and the number in {page}. React expects a title's children to be one string, and it doesn't join a list of pieces. In React 19.3.1 the title comes out empty, with no warning in the browser. Rendered on the server, it comes out empty too, and React warns in development that the children are an array. Press Edit & run and change line 8 to <title>{`Search results, page ${page}`}</title>. Now the title is one string, and Next page updates it.
When the component that rendered a title goes away, React takes the title out of the head again. So you could let a cart drawer set the title while it's open:
1import { useState } from "react";2import HeadView from "./HeadView.jsx";3 4function CartDrawer({ count }) {5 return (6 <aside>7 <title>{`Your cart (${count}) | Tea Shop`}</title>8 <p>{count} items in your cart.</p>9 </aside>10 );11}12 13export default function App() {14 const [open, setOpen] = useState(false);15 return (16 <>17 <title>Tea Shop</title>18 <button onClick={() => setOpen(!open)}>{open ? "Close cart" : "Open cart"}</button>19 {open && <CartDrawer count={2} />}20 <HeadView />21 </>22 );23}Click Open cart. The tab text changes to Your cart (2) | Tea Shop, which looks right, but HeadView shows two titles in the head: line 7's and line 17's. React never merges titles. It puts a new title in front of the others, so document.title reads the newest, but search engines and other tools may read either. React's docs warn that browsers and search engines may then do anything. Click Close cart: React removes the drawer's title. The fix is to let one component decide: delete line 7 and change line 17 to <title>{open ? "Your cart (2) | Tea Shop" : "Tea Shop"}</title>.
Two kinds of tag look like metadata but describe part of the page, not the document, so React leaves them where they are:
1import HeadView from "./HeadView.jsx";2 3export default function App() {4 return (5 <article itemScope itemType="https://schema.org/Product">6 <meta itemProp="sku" content="TEA-GREEN-100" />7 <h1 itemProp="name">Green tea</h1>8 <svg width="32" height="32" role="img">9 <title>Organic</title>10 <circle cx="16" cy="16" r="14" fill="seagreen" />11 </svg>12 <HeadView />13 </article>14 );15}Line 6 is a <meta> with itemProp: it gives the product's stock number to tools that read product details from the page, so it belongs inside the <article>. Line 9 is a <title> inside an <svg>: it names the drawing for screen readers. HeadView says (nothing). Both stayed in the page.
Try every kind of tag in one place. Each button puts a tag in a ProductCard that renders twice:
function ProductCard() {
return (
<div className="card">
<title>Green tea | Tea Shop</title>
<h2>Green tea</h2>
</div>
);
}
export default function App() {
return (
<>
<ProductCard />
<ProductCard />
</>
);
}in <head>Added to <head>:
<title>Green tea | Tea Shop</title>
<title>Green tea | Tea Shop</title>Inside <div id="root">:
<div class="card"><h2>Green tea</h2></div>
<div class="card"><h2>Green tea</h2></div>Both titles move to <head>, and React keeps both: it never merges titles. Two at once is a mistake, so render one title per page.
Two things stand out. Titles, meta tags and ordinary links move to the head once per render, so two cards give two copies. Stylesheets with precedence and async scripts move there once per address. How to read the table: each row is a tag, when React moves it, and whether copies become one.
| Tag | React moves it when | Do copies become one? |
|---|---|---|
<title> | Always, unless it sits inside an <svg> or has itemProp. | No. Every rendered title is added. |
<meta> | Always, unless it has itemProp. | No. Every rendered meta tag is added. |
<link> that isn't a stylesheet | When it has rel and href, and no onLoad, onError or itemProp. | No. Every rendered link is added. |
<link rel="stylesheet"> | Only with precedence, and without onLoad, onError or disabled. | Yes. One per href. |
<style> | Only with both href and precedence. | Yes. One per href. |
<script> | Only with src and async, and without onLoad or onError. | Yes. One per src. |
The rules in this table are one function in React DOM, isHostHoistableType, at line 6357 of ReactFiberConfigDOM.js in v19.3.1. Its switch has a case for meta and title, one for style, one for link and one for script.
Stylesheets that load before their content shows
The Styling components lesson showed that a <style> with href and precedence goes to the head once. A <link rel="stylesheet"> with precedence does the same, with one more job: a file takes time to download, and React won't show the content that needs it before it has arrived. Click Show reviews and read the console under the result:
1import { Suspense, useLayoutEffect, useState } from "react";2import HeadView from "./HeadView.jsx";3 4// The preview has no file server, so the stylesheet is a data: URL: the CSS is the URL.5const reviewsCss = "data:text/css,.reviews{border-left:6px solid seagreen;padding-left:8px}";6 7function Reviews() {8 useLayoutEffect(() => {9 const border = getComputedStyle(document.querySelector(".reviews")).borderLeftWidth;10 console.log("Reviews on screen. Left border:", border);11 }, []);12 return (13 <>14 <link rel="stylesheet" href={reviewsCss} precedence="default" />15 <p className="reviews">5 stars from Asha: fresh and grassy.</p>16 </>17 );18}19 20export default function App() {21 const [open, setOpen] = useState(false);22 return (23 <>24 <button onClick={() => setOpen(true)}>Show reviews</button>25 <Suspense fallback={<p>Loading reviews...</p>}>{open && <Reviews />}</Suspense>26 <HeadView log />27 </>28 );29}Line 5 is the stylesheet. The preview has no web server for files, so it's a data: URL: the CSS is written inside the address itself. Line 14 links it with precedence="default". The layout effect on lines 8 to 11 runs right after the reviews reach the page, before the browser paints, and logs the width of their left border. Line 25 wraps the reviews in a Suspense boundary. Line 26 asks HeadView to log each tag added to the head.
The console shows three steps, in this order:
added to <head>: <link rel="preload" as="style" …>. React met the stylesheet while rendering and asked the browser to start fetching it. MeanwhileLoading reviews...shows for a moment: the boundary's fallback.added to <head>: <link rel="stylesheet" … data-precedence="default">. The file is there, so React inserted the real stylesheet link and waited for it to load.Reviews on screen. Left border: 6px. Only now did React put the reviews on the page, already styled. The console marks this line with a 2: StrictMode runs effects twice in development.
You click Show reviews. React renders Reviews and meets its stylesheet link.
If the click had been a Transition, React would have kept the old screen while it waited, as it does for any Suspense in a Transition. If the file never loads, React gives up after a minute and shows the content without it (the 60-second timeout in React DOM). Now press Edit & run and delete precedence="default" from line 14. The console shows no preload and no head link, because the link is now an ordinary tag inside the page, and React doesn't wait for it. A data: URL loads at once, so the border is still 6px here, but a real file over a slow network would leave the reviews unstyled for a moment.
precedence also decides the order of stylesheets in the head. When two stylesheets set the same property on the same element, the one later in the document wins, so the order matters:
1import HeadView from "./HeadView.jsx";2 3const cardCss = "data:text/css,.card{border:3px solid gray;padding:8px;margin:8px 0}";4const saleCss = "data:text/css,.card{border:3px dashed crimson}";5const ratingCss = "data:text/css,.rating{font-style:italic}";6 7function ProductCard({ name }) {8 return (9 <div className="card">10 <link rel="stylesheet" href={cardCss} precedence="base" />11 {name}12 </div>13 );14}15 16function SaleBanner() {17 return <link rel="stylesheet" href={saleCss} precedence="theme" />;18}19 20function Rating() {21 return (22 <p className="rating">23 <link rel="stylesheet" href={ratingCss} precedence="base" />24 Rated 4 out of 525 </p>26 );27}28 29export default function App() {30 return (31 <>32 <ProductCard name="Green tea" />33 <SaleBanner />34 <Rating />35 <ProductCard name="Black tea" />36 <HeadView />37 </>38 );39}Line 10 gives every ProductCard a gray solid border from cardCss, with precedence "base". Line 17 is a sale theme with a crimson dashed border for the same .card, with precedence "theme". Line 23 adds italics to the rating, in "base" again. App renders them in the order of lines 32 to 35, and ProductCard twice.
HeadView lists three stylesheet links, not four: the second ProductCard added nothing, because React keeps one stylesheet per href. They're grouped: cardCss and ratingCss together in base, then saleCss in theme. The names mean nothing to React. The precedence it meets first ranks lowest and goes first, and each later one goes after it. So theme comes last and wins: the cards have a dashed crimson border.
base first, so the base group comes first and theme after it. The second ProductCard adds nothing: card.css is already there. Later stylesheets win a clash, so sale.css's dashed border beats card.css's solid one.Press Edit & run and move <SaleBanner /> above the first <ProductCard />. Now React meets theme first, so theme ranks lowest, and the cards turn solid gray. Pick your precedence names once, for the whole app, and make sure the lowest one renders first; many apps need only one.
Async scripts that load once
Some components need a third-party script, like a map or a chat widget. Rendering the <script> inside the component that needs it keeps them together, but the same component may render many times. React 19 handles one kind of script for you: an async script with a src. async tells the browser it may run the script as soon as it downloads, in any order, so React can put it anywhere. Read the console under the result:
1import HeadView from "./HeadView.jsx";2 3// map.js as a data: URL. Each time it runs, it counts and logs.4const mapJs = "data:text/javascript,window.mapRuns=(window.mapRuns||0)+1;console.log('map.js has run',window.mapRuns,'time(s)')";5 6function StoreMap({ city }) {7 return (8 <div>9 <script async src={mapJs} />10 <p>Map of our {city} shop</p>11 </div>12 );13}14 15export default function App() {16 return (17 <>18 <StoreMap city="Pune" />19 <StoreMap city="Leeds" />20 <HeadView />21 </>22 );23}Line 4 is map.js, again as a data: URL. Each time it runs, it adds one to window.mapRuns and logs the count. Line 9 renders it with async, and lines 18 and 19 render two maps. The console says map.js has run 1 time(s), and HeadView shows one <script async=""> in the head. React kept one script per src.
Now press Edit & run and delete async from line 9. HeadView shows nothing, the script never runs, and the console shows React's warning: Scripts inside React components are never executed when rendering on the client. A script without async must run in a fixed order, so React leaves it where you wrote it, and a <script> element created by React in the browser doesn't run. An async script with onLoad or onError is left in place too, in React 19.3.1: React treats it as a tag you manage yourself, so it isn't moved to the head and, created in the browser, it doesn't run. A script you need on every page belongs in index.html; one that only some components need is an async script, or a preinit call from the next section.
Telling the browser about files early
A browser finds files as it reads: the HTML names a stylesheet, the stylesheet names a font, a script asks for an image. Each discovery waits for the file before it. react-dom exports six functions that name a file, or a server, earlier. Each one adds a resource hint tag to the head: an instruction the browser may act on before it needs the file.
1import { prefetchDNS, preconnect, preload, preinit, preloadModule, preinitModule } from "react-dom";2import HeadView from "./HeadView.jsx";3 4// The preview has no file server, so each file is a data: URL. The scripts log when they run.5const photo = "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='40' height='40'><circle cx='20' cy='20' r='18' fill='seagreen'/></svg>";6const chatJs = "data:text/javascript,console.log('chat.js ran')";7const checkoutJs = "data:text/javascript,console.log('checkout.js ran')";8const analyticsJs = "data:text/javascript,console.log('analytics.js ran')";9 10function ProductPage() {11 prefetchDNS("https://reviews.tea-shop.example");12 preconnect("https://images.tea-shop.example");13 preload(photo, { as: "image" });14 preload(photo, { as: "image" });15 preinit(chatJs, { as: "script" });16 preloadModule(checkoutJs, { as: "script" });17 preinitModule(analyticsJs, { as: "script" });18 return <h1>Green tea</h1>;19}20 21export default function App() {22 return (23 <>24 <ProductPage />25 <HeadView />26 </>27 );28}The preview has no file server, so lines 5 to 8 are data: URLs again: a small picture and three scripts that log when they run. ProductPage calls the six functions while it renders, lines 11 to 17, and HeadView shows the tags React added, one per call except line 14. Line 14 preloads the same picture as line 13, so it changes nothing: like stylesheets, each file is hinted once. Lines 11 and 12 name servers, not files; their addresses end in .example, a name reserved for examples, so the browser has nothing to connect to here.
Now read the console: it says chat.js ran and analytics.js ran, but never checkout.js ran. preinit (line 15) and preinitModule (line 17) download a script and run it. preloadModule (line 16), like preload, only downloads: the file waits until code imports it.
How to read the table: each row is a function, the tag React adds, what the browser does with it, and when to choose it.
| Function | Tag React adds | What the browser does | Use it when |
|---|---|---|---|
prefetchDNS(href) | <link rel="dns-prefetch"> | Looks up the server's address. | You might load something from that server. |
preconnect(href) | <link rel="preconnect"> | Opens a connection to the server. | You will load something from that server soon, but don't know which file yet. |
preload(href, { as }) | <link rel="preload"> | Downloads the file and keeps it, without using it. | You know a font, image, stylesheet or script the page will need. |
preinit(href, { as }) | <link rel="stylesheet"> or <script async> | Downloads the file and applies the stylesheet or runs the script. | The page needs it now, and it may take effect as soon as it arrives. |
preloadModule(href, { as: "script" }) | <link rel="modulepreload"> | Downloads an ES module and keeps it. | A module the page will import soon. |
preinitModule(href, { as: "script" }) | <script type="module" async> | Downloads an ES module and runs it. | A module that should run as soon as it arrives. |
preload names the font in the HTML, so both files download at once and the text can show sooner.You can call them while rendering, as above, when you know the component will need a file. You can also call them in an event handler, to start a download before a click even happens. Point at the button:
1import { preload } from "react-dom";2import HeadView from "./HeadView.jsx";3 4const reviewsCss = "data:text/css,.reviews{border-left:6px solid seagreen}";5 6export default function App() {7 return (8 <>9 <button onPointerEnter={() => preload(reviewsCss, { as: "style" })}>10 Show reviews11 </button>12 <HeadView log />13 </>14 );15}Line 9 calls preload on onPointerEnter, which fires when the mouse pointer moves onto the button. The console logs added to <head>: <link rel="preload" as="style" …> the first time, and nothing after that. By the time you click, the file is already on its way. Hints are suggestions: the browser decides whether to act on them. Don't hint everything, or the hints compete with the files the page needs first.
The same tags in server-rendered HTML
Everything so far ran in the browser. Metadata matters even more on the server: search engines and link previews read the HTML the server sends, often without running any JavaScript. React's server renderer, react-dom/server, hoists the same tags. It runs in a browser too, which makes it easy to watch. This example renders a whole document to a string and shows it, one tag per line:
1import { renderToString } from "react-dom/server";2import { preconnect } from "react-dom";3 4function ProductPage() {5 preconnect("https://images.tea-shop.example");6 return (7 <main>8 <title>Green tea | Tea Shop</title>9 <meta name="description" content="Loose leaf green tea, 100 g" />10 <link rel="stylesheet" href="/product.css" precedence="default" />11 <script async src="/reviews-widget.js" />12 <h1>Green tea</h1>13 </main>14 );15}16 17function Document() {18 return (19 <html lang="en">20 <head>21 <meta charSet="utf-8" />22 </head>23 <body>24 <ProductPage />25 </body>26 </html>27 );28}29 30const html = renderToString(<Document />);31document.getElementById("root").textContent = html.replaceAll("><", ">\n<");Document (lines 17 to 28) renders the <html> itself, with a <head> that only holds the charset. ProductPage renders the title, the description, a stylesheet and an async script inside <main>, and calls preconnect on line 5. Line 30 renders the document to a string, the way Server rendering and hydration did.
In the result, every one of those tags is in the head, and <main> holds only the <h1>. The order isn't the order of the code: the preconnect and the stylesheet come first, then the script, then the title and the description. React writes the tags that let the browser start downloading as early as possible first. A stylesheet in the head also means the browser won't paint the page until it has loaded, so the first paint is already styled. When a later part of the page streams in with a stylesheet of its own, React adds that stylesheet before showing that part; the lesson on streaming server rendering covers streaming.
Frameworks build on this (the React frameworks lesson compares them). Next.js's App Router has its own way to set metadata: a page or layout exports a metadata object or a generateMetadata function, and Next.js writes the head tags. Both only work in Server Components, the next lesson:
// app/tea/[id]/page.jsx in a Next.js app
export async function generateMetadata({ params }) {
const { id } = await params;
const tea = await db.teas.find(id);
return { title: `${tea.name} | Tea Shop`, description: tea.summary };
}
export default async function TeaPage({ params }) {
const { id } = await params;
const tea = await db.teas.find(id);
return <h1>{tea.name}</h1>;
}Line 2 exports generateMetadata, which loads the tea on line 4 and returns its title and description on line 5. Next.js turns that object into <title> and <meta> tags. A framework API like this can merge metadata from nested layouts, which plain tags don't do: React's own blog notes you may still want a metadata library for that. In an app without a framework, or for a component library, the built-in tags are the tool. Either way, the react-dom resource functions work the same.
Everything a product page puts in the head
Here is a realistic product page with all of it together: the title, a description, the Open Graph tags that chat apps and social sites read for link previews, a canonical link that names the page's one true address for search engines, and its stylesheet.
1import HeadView from "./HeadView.jsx";2 3const productCss = "data:text/css,.price{font-weight:bold}";4 5function ProductPage({ tea }) {6 return (7 <main>8 <title>{`${tea.name} | Tea Shop`}</title>9 <meta name="description" content={tea.summary} />10 <meta property="og:title" content={tea.name} />11 <meta property="og:image" content={tea.image} />12 <link rel="canonical" href={`https://tea-shop.example/tea/${tea.id}`} />13 <link rel="stylesheet" href={productCss} precedence="default" />14 <h1>{tea.name}</h1>15 <p className="price">${tea.price}</p>16 </main>17 );18}19 20const greenTea = {21 id: "green",22 name: "Green tea",23 summary: "Loose leaf green tea, 100 g",24 image: "https://images.tea-shop.example/green-tea.jpg",25 price: 4,26};27 28export default function App() {29 return (30 <>31 <ProductPage tea={greenTea} />32 <HeadView />33 </>34 );35}Line 8 is the title. Lines 9 to 11 are meta tags: the description, and two Open Graph tags, which use property instead of name and move to the head the same way. Line 12 is the canonical link, and line 13 the page's stylesheet. Only lines 14 and 15 stay in <main>.
There's no Suspense boundary around ProductPage, so while productCss loads, React shows nothing at all: the whole first render waits. That's fine for a page's main stylesheet. For a part that can come later, like the reviews, give it a boundary of its own.
<title>Green tea | Tea Shop</title><meta name="description" content="Loose leaf" /><link rel="canonical" href="https://tea-shop.example/tea/green" /><link rel="stylesheet" href="/card.css" precedence="default" /><script async src="/map.js" /><link rel="stylesheet" href="/card.css" /><meta itemProp="sku" content="TEA-1" /><svg><title>Organic</title></svg><script async src="/map.js" onLoad={showMap} />
Sort each tag: does React 19 move it into <head>, or leave it where you wrote it?
Common mix-ups
- "A component that renders <title> needs a library like react-helmet." No. Since React 19,
<title>,<meta>and<link>work from any component, in the browser and on the server. - "React merges the titles of nested components." No. Every rendered title is added to the head. Render exactly one at a time.
- "Every <link rel="stylesheet"> goes to the head." No. Only one with
precedence. Without it, the link stays where you wrote it, once per render, and React doesn't wait for it. - "precedence="high" always beats precedence="low"." No. The names are labels. The precedence React meets first ranks lowest.
- "Any <script> in a component runs once." No. Only an async script with a
srcis moved and loaded once. A script withoutasyncthat React creates in the browser never runs. - "preload runs the script." No.
preloadonly downloads it.preinitdownloads and runs it.
Practice exercises
Each exercise has a live result, or gets one when you press Edit & run. HeadView lists what React added to the head, so you can check your answer there.
Make the title say which page of results you're on. What does document.title say before you click?
1import { useState } from "react";2import HeadView from "./HeadView.jsx";3 4export default function App() {5 const [page, setPage] = useState(1);6 return (7 <>8 <title>Search results, page {page}</title>9 <button onClick={() => setPage(page + 1)}>Next page</button>10 <p>Showing page {page} of the tea search.</p>11 <HeadView />12 </>13 );14}1import { useState } from "react";2import HeadView from "./HeadView.jsx";3 4export default function App() {5 const [page, setPage] = useState(1);6 return (7 <>8 <title>{`Search results, page ${page}`}</title>9 <button onClick={() => setPage(page + 1)}>Next page</button>10 <p>Showing page {page} of the tea search.</p>11 <HeadView />12 </>13 );14}The template literal on line 8 turns the text and the number into one string, so the title is Search results, page 1, and Next page changes it to page 2.
Give the cart page a description for search engines. How many tags does HeadView list then?
1import HeadView from "./HeadView.jsx";2 3export default function CartPage() {4 return (5 <main>6 <title>Your cart | Tea Shop</title>7 <h1>Your cart</h1>8 <p>2 items, $7 in total.</p>9 <HeadView />10 </main>11 );12}1import HeadView from "./HeadView.jsx";2 3export default function CartPage() {4 return (5 <main>6 <title>Your cart | Tea Shop</title>7 <meta name="description" content="Check your tea order and pay." />8 <h1>Your cart</h1>9 <p>2 items, $7 in total.</p>10 <HeadView />11 </main>12 );13}The new <meta> on line 7 moves to the head beside the title, so HeadView lists two tags. In a real app, the description is what a search result may show under the link.
Read the code and predict: how many <link rel="stylesheet"> tags does React put in the head? Then press Edit & run to check with HeadView.
import HeadView from "./HeadView.jsx";
const cardCss = "data:text/css,.card{padding:8px}";
const badgeCss = "data:text/css,.badge{font-weight:bold}";
function SaleBadge() {
return (
<span className="badge">
<link rel="stylesheet" href={badgeCss} precedence="base" />
Sale
</span>
);
}
function ProductCard({ name, onSale }) {
return (
<div className="card">
<link rel="stylesheet" href={cardCss} precedence="base" />
{name} {onSale && <SaleBadge />}
</div>
);
}
export default function App() {
return (
<>
<ProductCard name="Green tea" onSale />
<ProductCard name="Black tea" />
<ProductCard name="Mint tea" onSale />
<HeadView />
</>
);
}1import HeadView from "./HeadView.jsx";2 3const cardCss = "data:text/css,.card{padding:8px}";4const badgeCss = "data:text/css,.badge{font-weight:bold}";5 6function SaleBadge() {7 return (8 <span className="badge">9 <link rel="stylesheet" href={badgeCss} precedence="base" />10 Sale11 </span>12 );13}14 15function ProductCard({ name, onSale }) {16 return (17 <div className="card">18 <link rel="stylesheet" href={cardCss} precedence="base" />19 {name} {onSale && <SaleBadge />}20 </div>21 );22}23 24export default function App() {25 return (26 <>27 <ProductCard name="Green tea" onSale />28 <ProductCard name="Black tea" />29 <ProductCard name="Mint tea" onSale />30 <HeadView />31 </>32 );33}Three cards render cardCss and two sale badges render badgeCss: five links, two addresses. React keeps one per href, so the head gets two stylesheet links, both in the base group.
The sale banner now renders first, and the cards lost their sale border. Make the theme win again without moving <SaleBanner />. What border style do the cards have then?
1import HeadView from "./HeadView.jsx";2 3const cardCss = "data:text/css,.card{border:3px solid gray;padding:8px;margin:8px 0}";4const saleCss = "data:text/css,.card{border:3px dashed crimson}";5const ratingCss = "data:text/css,.rating{font-style:italic}";6 7function ProductCard({ name }) {8 return (9 <div className="card">10 <link rel="stylesheet" href={cardCss} precedence="base" />11 {name}12 </div>13 );14}15 16function SaleBanner() {17 return <link rel="stylesheet" href={saleCss} precedence="theme" />;18}19 20function Rating() {21 return (22 <p className="rating">23 <link rel="stylesheet" href={ratingCss} precedence="base" />24 Rated 4 out of 525 </p>26 );27}28 29export default function App() {30 return (31 <>32 <SaleBanner />33 <ProductCard name="Green tea" />34 <Rating />35 <ProductCard name="Black tea" />36 <HeadView />37 </>38 );39}1import HeadView from "./HeadView.jsx";2 3const cardCss = "data:text/css,.card{border:3px solid gray;padding:8px;margin:8px 0}";4const saleCss = "data:text/css,.card{border:3px dashed crimson}";5const ratingCss = "data:text/css,.rating{font-style:italic}";6 7function ProductCard({ name }) {8 return (9 <div className="card">10 <link rel="stylesheet" href={cardCss} precedence="base" />11 {name}12 </div>13 );14}15 16function SaleBanner() {17 return <link rel="stylesheet" href={saleCss} precedence="theme" />;18}19 20function Rating() {21 return (22 <p className="rating">23 <link rel="stylesheet" href={ratingCss} precedence="base" />24 Rated 4 out of 525 </p>26 );27}28 29export default function App() {30 return (31 <>32 <link rel="stylesheet" href={cardCss} precedence="base" />33 <SaleBanner />34 <ProductCard name="Green tea" />35 <Rating />36 <ProductCard name="Black tea" />37 <HeadView />38 </>39 );40}The new link at the top of App is the first stylesheet React meets, so base ranks lowest and theme comes after it. The sale stylesheet wins the clash again, and the cards get a dashed crimson border. Moving <SaleBanner /> back below the first card works too.
Neither map loads its script. Fix it so map.js runs. How many times does it run with two maps?
1import HeadView from "./HeadView.jsx";2 3// map.js as a data: URL. Each time it runs, it counts and logs.4const mapJs = "data:text/javascript,window.mapRuns=(window.mapRuns||0)+1;console.log('map.js has run',window.mapRuns,'time(s)')";5 6function StoreMap({ city }) {7 return (8 <div>9 <script src={mapJs} />10 <p>Map of our {city} shop</p>11 </div>12 );13}14 15export default function App() {16 return (17 <>18 <StoreMap city="Pune" />19 <StoreMap city="Leeds" />20 <HeadView />21 </>22 );23}1import HeadView from "./HeadView.jsx";2 3// map.js as a data: URL. Each time it runs, it counts and logs.4const mapJs = "data:text/javascript,window.mapRuns=(window.mapRuns||0)+1;console.log('map.js has run',window.mapRuns,'time(s)')";5 6function StoreMap({ city }) {7 return (8 <div>9 <script async src={mapJs} />10 <p>Map of our {city} shop</p>11 </div>12 );13}14 15export default function App() {16 return (17 <>18 <StoreMap city="Pune" />19 <StoreMap city="Leeds" />20 <HeadView />21 </>22 );23}With async and a src, React moves the script to the head and keeps one per src. Two maps, one script, and the console says it ran once.
Start fetching the reviews stylesheet as soon as the pointer moves onto Show reviews. What rel does the tag that appears on hover have?
1import { Suspense, useState } from "react";2import HeadView from "./HeadView.jsx";3 4const reviewsCss = "data:text/css,.reviews{border-left:6px solid seagreen;padding-left:8px}";5 6function Reviews() {7 return (8 <>9 <link rel="stylesheet" href={reviewsCss} precedence="default" />10 <p className="reviews">5 stars from Asha: fresh and grassy.</p>11 </>12 );13}14 15export default function App() {16 const [open, setOpen] = useState(false);17 return (18 <>19 <button onClick={() => setOpen(true)}>Show reviews</button>20 <Suspense fallback={<p>Loading reviews...</p>}>{open && <Reviews />}</Suspense>21 <HeadView log />22 </>23 );24}1import { Suspense, useState } from "react";2import { preload } from "react-dom";3import HeadView from "./HeadView.jsx";4 5const reviewsCss = "data:text/css,.reviews{border-left:6px solid seagreen;padding-left:8px}";6 7function Reviews() {8 return (9 <>10 <link rel="stylesheet" href={reviewsCss} precedence="default" />11 <p className="reviews">5 stars from Asha: fresh and grassy.</p>12 </>13 );14}15 16export default function App() {17 const [open, setOpen] = useState(false);18 return (19 <>20 <button21 onPointerEnter={() => preload(reviewsCss, { as: "style" })}22 onClick={() => setOpen(true)}23 >24 Show reviews25 </button>26 <Suspense fallback={<p>Loading reviews...</p>}>{open && <Reviews />}</Suspense>27 <HeadView log />28 </>29 );30}When the pointer reaches the button, preload adds a <link rel="preload" as="style"> and the browser starts fetching the file. By the time you click, it may have arrived, so the fallback shows for less time or not at all.
Check your understanding
For each question, ask two things: does React move this tag, and does it keep one copy or every copy?
Question 1 of 7Where does React 19 put this
<title>whenProductPagerenders in the browser?Read the code, then predictPop out in the code editor (opens in a new tab)JSXfunction ProductPage() { return ( <main> <title>Green tea | Tea Shop</title> <h1>Green tea</h1> </main> ); }Choose an answer to see the explanation.
Question 2 of 7
pageis 2. What does the document's title say?Read the code, then predictPop out in the code editor (opens in a new tab)JSXfunction Results({ page }) { return <title>Results, page {page}</title>; }Choose an answer to see the explanation.
Question 3 of 7These three links render in this order. In what order does React put them in
<head>?Read the code, then predictPop out in the code editor (opens in a new tab)JSXfunction App() { return ( <> <link rel="stylesheet" href="/layout.css" precedence="low" /> <link rel="stylesheet" href="/theme.css" precedence="high" /> <link rel="stylesheet" href="/cards.css" precedence="low" /> </> ); }Choose an answer to see the explanation.
Question 4 of 7Three product cards each render
<link rel="stylesheet" href="/card.css" precedence="base" />. How many links to card.css end up in<head>?Choose an answer to see the explanation.
Question 5 of 7What happens to
/map.jswhen this component renders in the browser?Read the code, then predictPop out in the code editor (opens in a new tab)JSXfunction StoreMap() { return ( <div> <script src="/map.js" /> <p>Map of our Pune shop</p> </div> ); }Choose an answer to see the explanation.
Question 6 of 7The product page will need
/fonts/serif.woff2as soon as its stylesheet loads. Which call fits?Choose an answer to see the explanation.
Question 7 of 7A click shows a
Reviewscomponent that renders<link rel="stylesheet" href="/reviews.css" precedence="default" />. What shows until the file has loaded?Choose an answer to see the explanation.
Key takeaways
- In React 19, any component can render
<title>,<meta>and<link>. React DOM puts them in the head, in the browser and in server-rendered HTML, and removes them when the component goes away. - A title needs one string, and the page needs one title at a time. React never merges titles or meta tags.
- A stylesheet link with
precedencegoes to the head once perhref, grouped by precedence, the first one React meets lowest. Content that needs it waits, behind a Suspense fallback, until it has loaded. - An async script with a
srcgoes to the head once. A script withoutasynccreated by React never runs. prefetchDNS,preconnect,preload,preinit,preloadModuleandpreinitModuletell the browser about servers and files early, once each, from rendering or from event handlers.
Remember the one-liner.
Render the tags where they belong in your code; React puts them where they belong in the document.
Coming next: Server Components. Components that run only on the server, read data directly, and send none of their code to the browser.