mrkeyoor.com_
Wed 23 Sept 00:33 UTC
npmWeb Frontendupdated 22 Sept 2026

react-helmet review

react-helmet lets React components declare title, meta, link, script, style, noscript, html, and body changes. Mounted Helmet components are reduced together, so a nested route can replace a layout title or description while duplicates inside one Helmet survive. Legacy server rendering extracts the collected state with Helmet.renderStatic(). Version 6.1.0, released in 2020, restored the default export removed in 6.0 and updated react-fast-compare for Preact support. The design predates concurrent SSR, streaming, Server Components, and framework metadata APIs. Our package check found working CommonJS and ESM imports but no bundled TypeScript declarations.

Verdict

react-helmet 6.1.0 put 9 packages and 1 MB on disk in our sandbox, but it has had no npm release since 2020 and its transitive peer range stops at React 18. Keep it in a working legacy SPA; choose request-scoped or framework-native metadata for new React and SSR code.

We installed it

Lab card: what happened when we installed react-helmetScreenshot of react-helmet documentation
Install✓ · 1.3s9 packages on disk · 1 MB
ImportESM import works · require() works · CommonJS package
Browser8.9 KBgzipped (24.6 KB minified), bundled with esbuild
Typesno TypeScript types found
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does react-helmet install cleanly?

Yes. In a fresh container with an empty cache, npm install react-helmet finished in 1 seconds, leaving 9 packages and 1 MB on disk. npm audit reported no known vulnerabilities.

How much does react-helmet add to a browser bundle?

8.9 KB gzipped (24.6 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.

Does react-helmet work with both ESM and CommonJS?

Yes. Both import 'react-helmet' and require('react-helmet') worked in Node 22 in our run. The package is published as CommonJS.

Does react-helmet include TypeScript types?

No type declarations were found in our install, so TypeScript users need their own declarations.

react-helmet or react-helmet-async: which should you use?

react-helmet-async: Use it for a close JSX API with request-scoped server state and current React peers. react-helmet 6.1.0 put 9 packages and 1 MB on disk in our sandbox, but it has had no npm release since 2020 and its transitive peer range stops at React 18.

When should you not use react-helmet?

You are starting a React 19 project; react-side-effect 2.1.2 only declares React 16 through 18 and causes a documented peer conflict

API stability4/5The Helmet JSX vocabulary, nested override rules, titleTemplate, defaultTitle, defer, onChangeClientState, and renderStatic result have stayed recognizable for years. Version 6.1.0 even restored the default import alongside the named one. That unchanged surface helps legacy apps, but it cannot promise runtime compatibility: react-side-effect 2.1.2 excludes React 19 from its peer range, and the process-global server model conflicts with newer concurrent execution.
Docs3/5The README shows all 7 supported head element types, html and body attributes, nested replacement, title templates, deferred updates, client callbacks, string SSR output, component SSR output, and the same-instance bundling requirement. It plainly warns that omitting renderStatic leaks memory. The guide has no current React support matrix, React 19 workaround, TypeScript installation, concurrent-request analysis, streaming path, Server Component guidance, or migration page for active alternatives.
Maintenance1/5npm published version 6.1.0 on June 8, 2020, and GitHub's latest push is dated July 18, 2023. The repository is unarchived but carries 145 open issues and 55 pull requests in the first 200 open records, including issue 716 for the React 19 transitive peer conflict. No published update addresses React 19, concurrent SSR, streaming, or Server Components, so teams are adopting a frozen implementation rather than an actively adapted one.
Ecosystem4/5npm counted 3,489,406 downloads for the week ending August 24, 2026, and GitHub lists 17,459 stars. Years of React examples, router integrations, and developer familiarity make maintenance easy to search. Our install confirmed import and require interoperability. The ecosystem has moved around the package, though: declarations live in DefinitelyTyped, React 19 needs peer overrides or a replacement, and current SSR frameworks own their metadata lifecycle directly.

Use it if

  • An existing React 16 to 18 single-page app already uses Helmet and only needs maintenance
  • Nested routes must override a parent title, description, canonical URL, or social tags
  • A legacy synchronous server renderer can call renderStatic immediately after each request render
  • One familiar component should own head tags plus html and body attributes in an older stack
Skip it if

Setup reality

Our Node 22 installation of react-helmet 6.1.0 completed in 1.3 seconds. It left 9 packages and 1 MB on disk, and npm audit found 0 known vulnerabilities. The package itself is 120 KB unpacked with 4 direct dependencies and 1 peer dependency. It is CommonJS without an exports map, though require() and ESM import both worked. No TypeScript types ship. Our all-exports browser build measured 24.6 KB minified and 8.9 KB gzipped.

Client rendering needs no provider. A later or deeper Helmet replaces matching title and meta records, while duplicate links declared together can remain. DOM writes are deferred through requestAnimationFrame unless defer={false}; immediate mode can help tests or background tabs but makes the mutation synchronous. TypeScript users normally install @types/react-helmet separately. React 19 is a packaging trap: react-helmet declares React >=16.3, yet its react-side-effect dependency accepts only React 16, 17, or 18 and can make npm reject the tree.

Server rendering requires renderToString or renderToStaticMarkup first, followed by Helmet.renderStatic(). Place each returned title, meta, link, script, style, noscript, htmlAttributes, and bodyAttributes fragment into the document. The README warns that skipping renderStatic leaks accumulated mounted instances. The collection is process-global and renderStatic clears it, which means concurrent requests can mix or erase each other's head state. A server bundle must also resolve one react-helmet copy; the project recommends externalizing it so app and document code share the same instance.

Version 6.1.0 restored both default and named imports and updated its equality dependency for Preact. It did not add request-scoped state, React 19 peers, streaming support, or Server Components. Keep JSON-LD serialization safe by escaping less-than characters in user-derived values, and apply Content Security Policy rules to inline scripts separately. Social crawlers usually inspect initial HTML, so client-only tags are unreliable for previews. For new SSR work, react-helmet-async or the framework's own metadata pipeline avoids the global extraction contract.

Patterns

Set the route title set-title

import { Helmet } from 'react-helmet'

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

A later or deeper Helmet can replace the title. Keep untrusted values as React text rather than inserting HTML.

Write description and robots tags set-description

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

Nested matching meta tags are replaced. Check the initial server HTML because some crawlers never wait for a client update.

Apply a layout title suffix use-title-template

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

// Route.jsx
<Helmet><title>Pricing</title></Helmet>

The %s slot receives the nested title. defaultTitle is used when no child supplies one.

Declare one canonical URL set-canonical

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

Helmet adds the element but does not normalize the path, remove query tracking, or protect the destination host.

Render share-preview metadata set-social-tags

<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 an absolute image URL and render all 4 tags into the initial document for social crawlers.

Change html and body attributes set-document-attributes

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

Server output exposes htmlAttributes and bodyAttributes separately. Multiple client owners of body.className can overwrite each other.

Serialize JSON-LD without a script break-out add-json-ld

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

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

Escaping less-than prevents a user value from closing the script element. Content Security Policy still needs to permit this inline block.

Keep several locale alternatives add-alternate-links

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

Links in the same Helmet are preserved together. Each React element still needs a stable key.

Drain Helmet after a legacy server render extract-server-head

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 render. It clears global state, so this pattern is unsafe when 2 server requests render concurrently.

Use extracted values as React elements render-head-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>
  )
}

Reuse one extracted object for the document. toComponent() here belongs to a custom legacy renderer, not React Server Components.

Apply a client title synchronously disable-defer

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

The default schedules DOM changes with requestAnimationFrame. defer false can help background tabs and tests but performs work immediately.

Inspect a completed DOM update observe-client-change

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

This callback follows client head mutation. Setting state from it can create a render loop, so keep the observer side-effect bounded.

Alternatives

PackageRegistryPick it when
react-helmet-asyncnpmUse it for a close JSX API with request-scoped server state and current React peers
@unhead/reactnpmUse it for typed modern head management across client and server rendering
nextnpmUse Next.js metadata when the application already relies on its routing and server rendering

More web frontend guides

postcss · react · react-dom · tailwindcss · htmlparser2 · tailwind-merge · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.