react-helmet, react-helmet-async, and react-meta-tags are libraries designed to manage the <head> section of a document in React applications. They allow developers to dynamically update titles, meta descriptions, Open Graph tags, and link resources based on the current component tree. While they share the same core goal of improving SEO and social sharing previews, they differ significantly in how they handle server-side rendering (SSR), hydration, and long-term maintenance.
Managing document head metadata is critical for SEO, social sharing, and browser behavior. react-helmet, react-helmet-async, and react-meta-tags all solve this problem, but they handle the tricky parts of React — like server-side rendering (SSR) and hydration — in very different ways. Let's break down the technical trade-offs.
react-helmet was the original standard for years.
// react-helmet: Original implementation
import { Helmet } from 'react-helmet';
function Page() {
return (
<Helmet>
<title>My Page</title>
</Helmet>
);
}
react-helmet-async is a fork created to address the limitations of the original.
// react-helmet-async: Modern fork
import { Helmet } from 'react-helmet-async';
function Page() {
return (
<Helmet>
<title>My Page</title>
</Helmet>
);
}
react-meta-tags is a lighter alternative.
// react-meta-tags: Lightweight alternative
import MetaTags from 'react-meta-tags';
function Page() {
return (
<MetaTags>
<title>My Page</title>
</MetaTags>
);
}
This is the biggest differentiator. If your app renders on the server, this choice matters.
react-helmet requires manual extraction of head data on the server.
Helmet.renderStatic() after rendering your app to string.// react-helmet: SSR Extraction
import { renderToString } from 'react-dom/server';
import { Helmet } from 'react-helmet';
const html = renderToString(<App />);
const helmet = Helmet.renderStatic();
// Inject helmet.title.toString() into your HTML template
react-helmet-async uses a Context Provider to manage state.
HelmetProvider to track changes asynchronously.// react-helmet-async: SSR with Provider
import { HelmetProvider } from 'react-helmet-async';
import { renderToString } from 'react-dom/server';
const helmetContext = {};
const html = renderToString(
<HelmetProvider context={helmetContext}>
<App />
</HelmetProvider>
);
// Access helmetContext.helmet for head tags
react-meta-tags is primarily designed for Client-Side Rendering.
// react-meta-tags: CSR Focused
// No built-in SSR extraction method like Helmet
// Tags render directly to the DOM on the client
function Page() {
return <MetaTags><title>Client Only</title></MetaTags>;
}
Hydration is when React takes over static HTML in the browser. Mismatches cause flickers or errors.
react-helmet often causes hydration warnings.
// react-helmet: Potential Hydration Warning
// Console: Warning: Text content did not match.
// Server: <title>App</title>
// Client: <title>Loading...</title> (before Helmet updates)
react-helmet-async minimizes hydration mismatches.
// react-helmet-async: Smoother Hydration
// Handles async updates without triggering immediate DOM mismatches
// Reduces console warnings during development
react-meta-tags renders immediately on mount.
// react-meta-tags: Immediate Render
// Updates DOM as soon as component mounts
// May cause flicker if parent components re-render often
Despite their differences, all three libraries aim to solve the same core problem.
document.head manually.// All packages support declarative syntax
// react-helmet & react-helmet-async
<Helmet><title>Title</title></Helmet>
// react-meta-tags
<MetaTags><title>Title</title></MetaTags>
<title>, <meta>, <link>, and <script>.// Common meta tag usage across all libraries
<meta name="description" content="Page description" />
<meta property="og:image" content="/image.png" />
// Nested override behavior
// Parent sets title "Site"
// Child sets title "Page" -> Final title is "Page"
| Feature | react-helmet | react-helmet-async | react-meta-tags |
|---|---|---|---|
| Maintenance | ⚠️ Slow / Legacy | ✅ Active / Recommended | ⚠️ Intermittent |
| SSR Support | 🛠️ Manual (renderStatic) | ✅ Robust (HelmetProvider) | ❌ Limited / CSR Only |
| Hydration | ⚠️ Prone to mismatches | ✅ Optimized for async | ⚠️ Immediate render |
| API Style | <Helmet> | <Helmet> | <MetaTags> |
| Best Use Case | Legacy CSR Apps | Modern SSR/SSG Apps | Simple CSR Apps |
react-helmet-async is the clear winner for modern development 🏆. It fixes the hard problems of SSR and hydration without changing the API you already know. If you are starting a new project or using Next.js, Remix, or Gatsby, this is the default choice.
react-helmet is still functional but shows its age 🕰️. Use it only if you are maintaining an older codebase where refactoring isn't an option. Do not start new projects with it.
react-meta-tags has its place in simple tools 🛠️. If you are building a small dashboard or internal tool that never renders on the server, it works fine. But for public-facing sites needing SEO, the Helmet ecosystem is stronger.
Final Thought: The cost of switching from react-helmet to react-helmet-async is low — often just wrapping your app in a provider. The benefit — fewer bugs and better SSR support — is high. Make the switch.
Choose react-helmet only for legacy client-side rendered (CSR) applications where migrating to a newer library is not feasible. Avoid using it for new projects, especially those with server-side rendering, due to known hydration mismatches and slower maintenance cycles compared to modern alternatives.
Choose react-helmet-async for any new project, particularly those utilizing server-side rendering (SSR) or static site generation (SSG). It is the community-standard fork that resolves hydration issues found in the original package and offers better compatibility with modern React concurrency features.
Choose react-meta-tags for simple applications that only need basic meta tag injection without complex nested head management or SSR requirements. It provides a lightweight solution for client-side apps but lacks the deep ecosystem integration and SSR robustness of the Helmet family.
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
