react-document-title, react-helmet, and react-helmet-async are React components designed to manage the document head dynamically. They allow developers to change the page title, meta tags, links, and scripts based on the current application state or route. While react-document-title focuses solely on the page title, react-helmet and react-helmet-async provide full control over the entire <head> section, including SEO-critical meta tags. The key distinction lies in how they handle server-side rendering (SSR) and asynchronous component rendering, which impacts performance and correctness in modern React architectures.
All three packages aim to solve the same problem: managing the <head> section of your HTML document from within React components. However, they differ significantly in scope, server-side rendering (SSR) support, and architectural requirements. Let's compare how they handle common engineering challenges.
react-document-title manages only the document.title.
// react-document-title: Title only
import DocumentTitle from 'react-document-title';
function HomePage() {
return (
<DocumentTitle title="Home Page">
<div>Welcome</div>
</DocumentTitle>
);
}
react-helmet manages the entire <head> section.
// react-helmet: Full head control
import { Helmet } from 'react-helmet';
function HomePage() {
return (
<>
<Helmet>
<title>Home Page</title>
<meta name="description" content="Welcome to our site" />
</Helmet>
<div>Welcome</div>
</>
);
}
react-helmet-async also manages the entire <head> section.
react-helmet for component usage.// react-helmet-async: Full head control with Provider
import { Helmet, HelmetProvider } from 'react-helmet-async';
function App() {
return (
<HelmetProvider>
<HomePage />
</HelmetProvider>
);
}
function HomePage() {
return (
<>
<Helmet>
<title>Home Page</title>
<meta name="description" content="Welcome to our site" />
</Helmet>
<div>Welcome</div>
</>
);
}
react-document-title has limited SSR support.
// react-document-title: SSR extraction
import DocumentTitle from 'react-document-title';
// Server-side code
const title = DocumentTitle.rewind();
// Returns the title string set during render
react-helmet supports SSR but has known limitations.
rewind() method to extract head data after render.// react-helmet: SSR extraction
import { Helmet } from 'react-helmet';
// Server-side code
const helmet = Helmet.renderStatic();
// Returns object with title, meta, link, etc.
res.send(`<html>${helmet.title.toString()}...</html>`);
react-helmet-async is built for async SSR.
HelmetProvider to track head changes across async boundaries.helmetContext.helmet.toHead() on the server to get the data.// react-helmet-async: Async SSR extraction
import { HelmetProvider } from 'react-helmet-async';
// Server-side code
const helmetContext = {};
const app = renderToString(
<HelmetProvider context={helmetContext}>
<App />
</HelmetProvider>
);
const head = helmetContext.helmet.toHead();
// Safely extracts head data even with Suspense or async components
react-document-title requires no global setup.
// react-document-title: No provider needed
function App() {
return <DocumentTitle title="App">...</DocumentTitle>;
}
react-helmet requires no global setup for CSR.
// react-helmet: No provider needed for CSR
function App() {
return <Helmet><title>App</title></Helmet>;
}
react-helmet-async requires a top-level Provider.
HelmetProvider.react-helmet but ensures stability.// react-helmet-async: Provider required
import { HelmetProvider } from 'react-helmet-async';
function Root() {
return (
<HelmetProvider>
<App />
</HelmetProvider>
);
}
react-document-title is effectively legacy.
react-helmet is in maintenance mode.
react-helmet-async is the active standard for SSR.
react-helmet in async environments.Despite their differences, these libraries share core concepts and usage patterns.
// All packages use declarative JSX
<SomeHeadComponent title="My Title">
<Children />
</SomeHeadComponent>
// Dynamic update example (conceptual for all)
function ProductPage({ product }) {
return (
<HeadComponent title={product.name}>
<ProductDetails />
</HeadComponent>
);
}
// Automatic cleanup behavior
// When ProductPage unmounts, title reverts to previous state
| Feature | react-document-title | react-helmet | react-helmet-async |
|---|---|---|---|
| Scope | ๐ Title only | ๐ Full <head> | ๐ Full <head> |
| SSR Support | โ ๏ธ Limited/Static | โ ๏ธ Static (Issues with Async) | โ Full Async/Streaming |
| Setup | ๐งฉ Drop-in Component | ๐งฉ Drop-in Component | ๐งฉ Requires Provider |
| Maintenance | ๐ Legacy | ๐ก Maintenance Mode | ๐ข Active |
| Best For | ๐ฐ๏ธ Legacy Apps | ๐ฅ๏ธ Client-Side Only | ๐ SSR & Modern React |
react-document-title is a historical artifact ๐บ โ useful only for very specific, simple legacy cases where only the title matters. It lacks the features needed for modern web standards.
react-helmet is a reliable workhorse ๐ด for client-side applications. It is simple and effective if you do not need server-side rendering. However, its limitations in async environments make it risky for modern full-stack frameworks.
react-helmet-async is the modern engineering choice ๐ ๏ธ for full-stack React. The requirement for a Provider is a small price to pay for correct behavior during server-side rendering and streaming. It ensures your SEO tags and social cards render correctly regardless of how your data is fetched.
Final Thought: If your app renders on the server โ even partially โ choose react-helmet-async. If it is purely client-side, react-helmet is acceptable, but react-helmet-async is still safer for future proofing. Avoid react-document-title for any new development.
Choose react-helmet if you are building a client-side rendered (CSR) application without server-side rendering requirements. It is a stable choice for standard single-page apps where the initial HTML head does not need to be pre-populated by the server. However, be aware that it may encounter issues with memory leaks or context loss in complex asynchronous SSR setups.
Choose react-document-title only if you are maintaining a legacy application that requires changing the page title and nothing else. It is not suitable for modern projects requiring SEO meta tags or server-side rendering support. For any new development, this package is considered outdated and should be avoided in favor of more capable alternatives.
Choose react-helmet-async for any project utilizing server-side rendering, static site generation, or asynchronous React features. It wraps the original react-helmet logic but adds a required Provider component to correctly handle context during async rendering. This is the industry standard for Next.js, Remix, or custom Node.js SSR implementations where accurate head data must be extracted on the server.
This reusable React component will manage all of your changes to the document head.
Helmet takes plain HTML tags and outputs plain HTML tags. It's dead simple, and React beginner friendly.
import React from "react";
import {Helmet} from "react-helmet";
class Application extends React.Component {
render () {
return (
<div className="application">
<Helmet>
<meta charSet="utf-8" />
<title>My Title</title>
<link rel="canonical" href="http://mysite.com/example" />
</Helmet>
...
</div>
);
}
};
Nested or latter components will override duplicate changes:
<Parent>
<Helmet>
<title>My Title</title>
<meta name="description" content="Helmet application" />
</Helmet>
<Child>
<Helmet>
<title>Nested Title</title>
<meta name="description" content="Nested component" />
</Helmet>
</Child>
</Parent>
outputs:
<head>
<title>Nested Title</title>
<meta name="description" content="Nested component">
</head>
See below for a full reference guide.
title, base, meta, link, script, noscript, and style tags.body, html and title tags.Helmet 5 is fully backward-compatible with previous Helmet releases, so you can upgrade at any time without fear of breaking changes. We encourage you to update your code to our more semantic API, but please feel free to do so at your own pace.
Yarn:
yarn add react-helmet
npm:
npm install --save react-helmet
To use on the server, call Helmet.renderStatic() after ReactDOMServer.renderToString or ReactDOMServer.renderToStaticMarkup to get the head data for use in your prerender.
Because this component keeps track of mounted instances, you have to make sure to call renderStatic on server, or you'll get a memory leak.
ReactDOMServer.renderToString(<Handler />);
const helmet = Helmet.renderStatic();
This helmet instance contains the following properties:
basebodyAttributeshtmlAttributeslinkmetanoscriptscriptstyletitleEach property contains toComponent() and toString() methods. Use whichever is appropriate for your environment. For attributes, use the JSX spread operator on the object returned by toComponent(). E.g:
const html = `
<!doctype html>
<html ${helmet.htmlAttributes.toString()}>
<head>
${helmet.title.toString()}
${helmet.meta.toString()}
${helmet.link.toString()}
</head>
<body ${helmet.bodyAttributes.toString()}>
<div id="content">
// React stuff here
</div>
</body>
</html>
`;
function HTML () {
const htmlAttrs = helmet.htmlAttributes.toComponent();
const bodyAttrs = helmet.bodyAttributes.toComponent();
return (
<html {...htmlAttrs}>
<head>
{helmet.title.toComponent()}
{helmet.meta.toComponent()}
{helmet.link.toComponent()}
</head>
<body {...bodyAttrs}>
<div id="content">
// React stuff here
</div>
</body>
</html>
);
}
If you are using a prebuilt compilation of your app with webpack in the server be sure to include this in the webpack file so that the same instance of react-helmet is used.
externals: ["react-helmet"],
Or to import the react-helmet instance from the app on the server.
<Helmet
{/* (optional) set to false to disable string encoding (server-only) */}
encodeSpecialCharacters={true}
{/*
(optional) Useful when you want titles to inherit from a template:
<Helmet
titleTemplate="%s | MyAwesomeWebsite.com"
>
<title>Nested Title</title>
</Helmet>
outputs:
<head>
<title>Nested Title | MyAwesomeWebsite.com</title>
</head>
*/}
titleTemplate="MySite.com - %s"
{/*
(optional) used as a fallback when a template exists but a title is not defined
<Helmet
defaultTitle="My Site"
titleTemplate="My Site - %s"
/>
outputs:
<head>
<title>My Site</title>
</head>
*/}
defaultTitle="My Default Title"
{/* (optional) callback that tracks DOM changes */}
onChangeClientState={(newState, addedTags, removedTags) => console.log(newState, addedTags, removedTags)}
>
{/* html attributes */}
<html lang="en" amp />
{/* body attributes */}
<body className="root" />
{/* title attributes and value */}
<title itemProp="name" lang="en">My Plain Title or {`dynamic`} title</title>
{/* base element */}
<base target="_blank" href="http://mysite.com/" />
{/* multiple meta elements */}
<meta name="description" content="Helmet application" />
<meta property="og:type" content="article" />
{/* multiple link elements */}
<link rel="canonical" href="http://mysite.com/example" />
<link rel="apple-touch-icon" href="http://mysite.com/img/apple-touch-icon-57x57.png" />
<link rel="apple-touch-icon" sizes="72x72" href="http://mysite.com/img/apple-touch-icon-72x72.png" />
{locales.map((locale) => {
<link rel="alternate" href="http://example.com/{locale}" hrefLang={locale} key={locale}/>
})}
{/* multiple script elements */}
<script src="http://include.com/pathtojs.js" type="text/javascript" />
{/* inline script elements */}
<script type="application/ld+json">{`
{
"@context": "http://schema.org"
}
`}</script>
{/* noscript elements */}
<noscript>{`
<link rel="stylesheet" type="text/css" href="foo.css" />
`}</noscript>
{/* inline style elements */}
<style type="text/css">{`
body {
background-color: blue;
}
p {
font-size: 12px;
}
`}</style>
</Helmet>
Please take a moment to review the guidelines for contributing.
MIT
