mrkeyoor.com_
Sat 08 Aug 21:58 UTC
npmWeb Frontendupdated 08 Aug 2026

react-helmet

react-helmet is a React component for changing document metadata from inside the component tree. Render ordinary title, meta, link, script, style, html, or body elements inside Helmet, and it updates the browser head after rendering. Nested Helmet instances are reduced so the deepest title or matching metadata wins, while same-component duplicates can remain. It also exposes renderStatic for extracting head markup after legacy server rendering. The idea is simple, but this package predates concurrent React and modern framework metadata APIs.

Verdict

Keep it when a stable legacy SPA already depends on its component API. Do not choose it for new React or concurrent SSR work; react-helmet-async is the least disruptive replacement, and a framework-native metadata API is better when available.

API stability4/5The JSX vocabulary, nesting rules, renderStatic result shape, title templates, and imperative renderStatic and peek methods have remained recognizable across major releases. Version 6.1.0 has not changed since June 2020, which makes legacy behavior predictable. That freeze does not guarantee compatibility with React's newer execution models, and the dependency-level React 19 peer conflict shows that an unchanged API can still become operationally incompatible.
Docs3/5The repository README provides a full reference example for every supported head tag, title templates, default titles, html and body attributes, callbacks, deferred updates, and both string and component forms for server output. It also gives unusually important warnings about renderStatic memory leaks and duplicate bundled instances. It does not provide a current React compatibility table, migration guidance, concurrent SSR model, TypeScript setup, or modern framework examples.
Maintenance1/5The current npm version, 6.1.0, was published on June 8, 2020, and GitHub reports the last push on July 18, 2023. The repository is not archived, but it carries 220 open issues and PRs, including an open React 19 peer-dependency report that remained active into 2026. No release has updated the package for React 19, concurrent SSR, streaming, or server components, so continued installation means accepting an effectively frozen implementation.
Ecosystem4/5react-helmet recorded 3,363,689 downloads from July 31 through August 6, 2026 and has 17,461 GitHub stars, so examples, integrations, and developer familiarity are abundant. Its plain JSX model works with many routers and older React stacks. The surrounding ecosystem has moved on, though: types live in DefinitelyTyped, React 19 exposes a transitive peer conflict, and frameworks now ship their own metadata pipelines.

Use it if

  • You are maintaining an existing React 16 to 18 single-page app that already uses Helmet successfully
  • You need nested route components to override a parent title, description, canonical link, or social metadata
  • Your legacy server renderer can call Helmet.renderStatic immediately after every render and processes requests without shared Helmet state
  • You need one component to manage title, meta, link, script, style, html attributes, and body attributes
Skip it if

Setup reality

Install react-helmet beside React; its declared React peer range starts at 16.3, and the package brings object-assign, prop-types, react-fast-compare, and react-side-effect. It ships CommonJS and an ES module build but no TypeScript declarations, so TypeScript projects normally add @types/react-helmet. Client-only use needs no provider: render Helmet anywhere under React and later or deeper instances override matching values. Server rendering is where the contract becomes fragile. You must render the app first, then call Helmet.renderStatic() exactly once for that render and place each returned title, meta, link, style, script, noscript, htmlAttributes, and bodyAttributes value into the HTML template. The README explicitly warns that failing to call renderStatic leaks the accumulated mounted-instance state. That state is process-global rather than request-scoped, which is a poor fit for concurrent SSR and streaming. Bundlers used for both client and server must also resolve one shared react-helmet instance; the README suggests making it a webpack external, because two copies split the tracked state. DOM updates defer through requestAnimationFrame by default, so tests or background-tab behavior may need defer={false}. On React 19, npm can report a peer conflict through react-side-effect even though react-helmet itself declares react >=16.3. Inline script and style content must be passed as strings, and adding JSON-LD or third-party scripts does not make that content safe under your Content Security Policy. This is maintenance territory, not a comfortable new dependency.

Patterns

Set a page titleset-page-title

import { Helmet } from 'react-helmet';

export function ProductPage({ product }) {
  return (
    <>
      <Helmet>
        <title>{product.name}</title>
      </Helmet>
      <h1>{product.name}</h1>
    </>
  );
}

A deeper or later mounted Helmet can override the title. Keep user-controlled title values as React text rather than injecting HTML.

Set description and robots metadataset-description

<Helmet>
  <meta name="description" content={summary} />
  <meta name="robots" content={isPublic ? 'index,follow' : 'noindex,nofollow'} />
</Helmet>

Matching meta tags from nested Helmet instances are replaced. Verify the final server HTML because client-only metadata arrives too late for some crawlers.

Apply a site-wide title templateuse-title-template

// Layout.jsx
<Helmet defaultTitle="Acme" titleTemplate="%s | Acme" />

// Route component
<Helmet>
  <title>Pricing</title>
</Helmet>

defaultTitle is used when no nested title exists. The %s placeholder in titleTemplate receives the child title.

Add a canonical linkset-canonical-url

<Helmet>
  <link rel="canonical" href={`https://example.com${canonicalPath}`} />
</Helmet>

Build an absolute, normalized URL yourself. Helmet inserts the tag but does not validate hosts, remove tracking parameters, or decide canonical policy.

Add Open Graph and Twitter card tagsset-social-cards

<Helmet>
  <meta property="og:title" content={title} />
  <meta property="og:description" content={description} />
  <meta property="og:image" content={absoluteImageUrl} />
  <meta name="twitter:card" content="summary_large_image" />
</Helmet>

Use absolute image URLs. Social crawlers usually read the initial HTML, so render these tags on the server rather than relying on a client update.

Update html and body attributesset-html-language

<Helmet>
  <html lang={locale} dir={isRtl ? 'rtl' : 'ltr'} />
  <body className="checkout-page" />
</Helmet>

On the server, htmlAttributes and bodyAttributes must be emitted separately from head tags. Client changes can conflict with code that also owns body classes.

Add structured data as JSON-LDadd-json-ld

const structuredData = {
  '@context': 'https://schema.org',
  '@type': 'Product',
  name: product.name,
};

<Helmet>
  <script type="application/ld+json">
    {JSON.stringify(structuredData).replace(/</g, '\u003c')}
  </script>
</Helmet>

Escape less-than characters when serializing data that could contain user input so a closing script sequence cannot break out of the element.

Emit alternate-language linksadd-alternate-locales

<Helmet>
  {locales.map(({ code, url }) => (
    <link key={code} rel="alternate" hrefLang={code} href={url} />
  ))}
</Helmet>

Stable React keys are still required. Multiple links declared in the same Helmet instance are preserved rather than collapsed.

Extract head markup after legacy SSRrender-on-server

import { renderToString } from 'react-dom/server';
import { Helmet } from 'react-helmet';

const appHtml = renderToString(<App url={request.url} />);
const head = Helmet.renderStatic();

const html = `<html ${head.htmlAttributes.toString()}>
<head>${head.title.toString()}${head.meta.toString()}${head.link.toString()}</head>
<body ${head.bodyAttributes.toString()}><div id="root">${appHtml}</div></body></html>`;

Call renderStatic after every server render. The README warns that omitting it leaks mounted-instance state; the global model is not suitable for concurrent streaming SSR.

Render extracted tags as React componentsrender-server-components

const head = Helmet.renderStatic();

function Document() {
  return (
    <html {...head.htmlAttributes.toComponent()}>
      <head>
        {head.title.toComponent()}
        {head.meta.toComponent()}
        {head.link.toComponent()}
      </head>
      <body {...head.bodyAttributes.toComponent()} />
    </html>
  );
}

Use the same extracted head object for the whole document. This pattern is for a custom legacy renderer, not React Server Components.

Apply client head changes immediatelyupdate-without-deferral

<Helmet defer={false}>
  <title>Upload complete</title>
</Helmet>

The default defers DOM work with requestAnimationFrame. Turning it off can help tests and background tabs but makes updates synchronous.

Observe client-side head updatesobserve-dom-changes

<Helmet
  onChangeClientState={(state, addedTags, removedTags) => {
    console.log(state.title, addedTags, removedTags);
  }}
>
  <title>{title}</title>
</Helmet>

The callback runs for client DOM changes, not as an analytics guarantee. Avoid setting state here in a way that creates a render loop.

Alternatives

PackageRegistryPick it when
react-helmet-asyncnpmUse it for a close Helmet-compatible API with per-request SSR context and declared React 19 support
@unhead/reactnpmUse it in a modern React 19 application that wants typed head management, plugins, and server rendering
nextnpmUse its built-in metadata system when the application is already on Next.js and needs streaming or server-component integration