🚨 Latest Research:Tanstack npm Packages Compromised in Ongoing Mini Shai-Hulud Supply-Chain Attack.Learn More
Socket
Book a DemoSign in
Socket

react-helmet-async

Package Overview
Dependencies
Maintainers
1
Versions
58
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

react-helmet-async - npm Package Compare versions

Comparing version
2.0.5
to
3.0.0
+38
lib/React19Dispatcher.d.ts
import React, { Component } from 'react';
import type { HelmetProps } from './types';
interface React19DispatcherProps extends HelmetProps {
/**
* The processed props including mapped children. These come from Helmet's
* mapChildrenToProps or the raw API props.
*/
[key: string]: any;
}
/**
* React 19+ Dispatcher: Instead of manual DOM manipulation, this component
* renders actual JSX elements. React 19 automatically hoists <title>, <meta>,
* <link>, <style>, and <script async> to <head>.
*
* For htmlAttributes and bodyAttributes, we still apply via direct DOM
* manipulation since React 19 doesn't handle those.
*/
export default class React19Dispatcher extends Component<React19DispatcherProps> {
componentDidMount(): void;
componentDidUpdate(): void;
componentWillUnmount(): void;
resolveTitle(): string | undefined;
renderTitle(): React.DetailedReactHTMLElement<{
[key: string]: any;
}, HTMLElement> | null;
renderBase(): React.DetailedReactHTMLElement<{
[key: string]: any;
}, HTMLElement> | null;
renderMeta(): React.DetailedReactHTMLElement<React.HTMLAttributes<HTMLElement>, HTMLElement>[] | null;
renderLink(): React.DetailedReactHTMLElement<React.HTMLAttributes<HTMLElement>, HTMLElement>[] | null;
renderScript(): React.DetailedReactHTMLElement<React.HTMLAttributes<HTMLElement>, HTMLElement>[] | null;
renderStyle(): React.DetailedReactHTMLElement<React.HTMLAttributes<HTMLElement>, HTMLElement>[] | null;
renderNoscript(): React.DetailedReactHTMLElement<React.HTMLAttributes<HTMLElement>, HTMLElement>[] | null;
render(): React.FunctionComponentElement<{
children?: React.ReactNode;
}>;
}
export {};
export declare const isReact19: boolean;
+2
-2
import { Component } from 'react';
import type { HelmetServerState } from './types';
export interface DispatcherContextProp {
setHelmet: (newState: HelmetServerState) => void;
setHelmet: (newState: HelmetServerState | null) => void;
helmetInstances: {

@@ -21,4 +21,4 @@ get: () => HelmetDispatcher[];

init(): void;
render(): any;
render(): null;
}
export {};

@@ -9,13 +9,13 @@ import type HelmetDispatcher from './Dispatcher';

interface HelmetDataContext {
helmet: HelmetServerState;
helmet: HelmetServerState | null;
}
export declare const isDocument: boolean;
export default class HelmetData implements HelmetDataType {
instances: any[];
instances: never[];
canUseDOM: boolean;
context: HelmetDataContext;
value: {
setHelmet: (serverState: HelmetServerState) => void;
setHelmet: (serverState: HelmetServerState | null) => void;
helmetInstances: {
get: () => any[];
get: () => HelmetDispatcher[];
add: (instance: HelmetDispatcher) => void;

@@ -22,0 +22,0 @@ remove: (instance: HelmetDispatcher) => void;

@@ -1,10 +0,7 @@

import type { PropsWithChildren, ReactElement, ReactNode } from 'react';
import type { PropsWithChildren } from 'react';
import React, { Component } from 'react';
import type { HelmetProps } from './types';
export * from './types';
export type { Attributes, BodyProps, HelmetDatum, HelmetHTMLBodyDatum, HelmetHTMLElementDatum, HelmetProps, HelmetServerState, HelmetTags, HtmlProps, LinkProps, MetaProps, StateUpdate, TagList, TitleProps, } from './types';
export { default as HelmetData } from './HelmetData';
export { default as HelmetProvider } from './Provider';
type Props = {
[key: string]: any;
};
export declare class Helmet extends Component<PropsWithChildren<HelmetProps>> {

@@ -17,23 +14,9 @@ static defaultProps: {

shouldComponentUpdate(nextProps: HelmetProps): boolean;
mapNestedChildrenToProps(child: ReactElement, nestedChildren: ReactNode): {
innerHTML: string | number | true | ReactElement<any, string | React.JSXElementConstructor<any>> | Iterable<ReactNode> | React.ReactPortal;
cssText?: undefined;
} | {
cssText: string | number | true | ReactElement<any, string | React.JSXElementConstructor<any>> | Iterable<ReactNode> | React.ReactPortal;
innerHTML?: undefined;
};
flattenArrayTypeChildren(child: JSX.Element, arrayTypeChildren: {
[key: string]: JSX.Element[];
}, newChildProps: Props, nestedChildren: ReactNode): {};
mapObjectTypeChildren(child: JSX.Element, newProps: Props, newChildProps: Props, nestedChildren: ReactNode): {};
mapArrayTypeChildrenToProps(arrayTypeChildren: {
[key: string]: JSX.Element;
}, newProps: Props): {
[x: string]: any;
};
warnOnInvalidChildren(child: JSX.Element, nestedChildren: ReactNode): boolean;
mapChildrenToProps(children: ReactNode, newProps: Props): {
[x: string]: any;
};
private mapNestedChildrenToProps;
private flattenArrayTypeChildren;
private mapObjectTypeChildren;
private mapArrayTypeChildrenToProps;
private warnOnInvalidChildren;
private mapChildrenToProps;
render(): React.JSX.Element;
}
// src/index.tsx
import React3, { Component as Component3 } from "react";
import React5, { Component as Component4 } from "react";
import fastCompare from "react-fast-compare";

@@ -7,3 +7,3 @@ import invariant from "invariant";

// src/Provider.tsx
import React2, { Component } from "react";
import React3, { Component } from "react";

@@ -376,4 +376,3 @@ // src/server.ts

let priorityMethods = {
toComponent: () => {
},
toComponent: () => [],
toString: () => ""

@@ -442,5 +441,10 @@ };

// src/reactVersion.ts
import React2 from "react";
var major = parseInt(React2.version.split(".")[0], 10);
var isReact19 = major >= 19;
// src/Provider.tsx
var defaultValue = {};
var Context = React2.createContext(defaultValue);
var Context = React3.createContext(defaultValue);
var HelmetProvider = class _HelmetProvider extends Component {

@@ -451,6 +455,13 @@ static canUseDOM = isDocument;

super(props);
this.helmetData = new HelmetData(this.props.context || {}, _HelmetProvider.canUseDOM);
if (isReact19) {
this.helmetData = null;
} else {
this.helmetData = new HelmetData(this.props.context || {}, _HelmetProvider.canUseDOM);
}
}
render() {
return /* @__PURE__ */ React2.createElement(Context.Provider, { value: this.helmetData.value }, this.props.children);
if (isReact19) {
return /* @__PURE__ */ React3.createElement(React3.Fragment, null, this.props.children);
}
return /* @__PURE__ */ React3.createElement(Context.Provider, { value: this.helmetData.value }, this.props.children);
}

@@ -478,7 +489,4 @@ };

} else if (attribute === "cssText" /* CSS_TEXT */) {
if (newElement.styleSheet) {
newElement.styleSheet.cssText = tag.cssText;
} else {
newElement.appendChild(document.createTextNode(tag.cssText));
}
const cssText = tag.cssText;
newElement.appendChild(document.createTextNode(cssText));
} else {

@@ -624,4 +632,3 @@ const attr = attribute;

helmetInstances.get().map((instance) => {
const props = { ...instance.props };
delete props.context;
const { context: _context, ...props } = instance.props;
return props;

@@ -655,4 +662,179 @@ })

// src/React19Dispatcher.tsx
import React4, { Component as Component3 } from "react";
var react19Instances = [];
var toHtmlAttributes = (props) => {
const result = {};
for (const key of Object.keys(props)) {
result[HTML_TAG_MAP[key] || key] = props[key];
}
return result;
};
var toReactProps = (attrs) => {
const result = {};
for (const key of Object.keys(attrs)) {
const mapped = REACT_TAG_MAP[key];
result[mapped || key] = attrs[key];
}
return result;
};
var applyAttributes = (tagName, attributes) => {
if (!isDocument)
return;
const el = document.getElementsByTagName(tagName)[0];
if (!el)
return;
const managedAttr = "data-rh-managed";
const prev = el.getAttribute(managedAttr);
const prevKeys = prev ? prev.split(",") : [];
const nextKeys = Object.keys(attributes);
for (const key of prevKeys) {
if (!nextKeys.includes(key)) {
el.removeAttribute(key);
}
}
for (const key of nextKeys) {
const value = attributes[key];
if (value === void 0 || value === null || value === false) {
el.removeAttribute(key);
} else if (value === true) {
el.setAttribute(key, "");
} else {
el.setAttribute(key, String(value));
}
}
if (nextKeys.length > 0) {
el.setAttribute(managedAttr, nextKeys.join(","));
} else {
el.removeAttribute(managedAttr);
}
};
var syncAllAttributes = () => {
const htmlAttrs = {};
const bodyAttrs = {};
for (const instance of react19Instances) {
const { htmlAttributes, bodyAttributes } = instance.props;
if (htmlAttributes) {
Object.assign(htmlAttrs, toHtmlAttributes(htmlAttributes));
}
if (bodyAttributes) {
Object.assign(bodyAttrs, toHtmlAttributes(bodyAttributes));
}
}
applyAttributes("html" /* HTML */, htmlAttrs);
applyAttributes("body" /* BODY */, bodyAttrs);
};
var React19Dispatcher = class extends Component3 {
componentDidMount() {
react19Instances.push(this);
syncAllAttributes();
}
componentDidUpdate() {
syncAllAttributes();
}
componentWillUnmount() {
const index = react19Instances.indexOf(this);
if (index !== -1) {
react19Instances.splice(index, 1);
}
syncAllAttributes();
}
resolveTitle() {
const { title, titleTemplate, defaultTitle } = this.props;
if (title && titleTemplate) {
return titleTemplate.replace(/%s/g, () => Array.isArray(title) ? title.join("") : title);
}
return title || defaultTitle || void 0;
}
renderTitle() {
const title = this.resolveTitle();
if (title === void 0)
return null;
const titleAttributes = this.props.titleAttributes || {};
return React4.createElement("title" /* TITLE */, toReactProps(titleAttributes), title);
}
renderBase() {
const { base } = this.props;
if (!base)
return null;
return React4.createElement("base" /* BASE */, toReactProps(base));
}
renderMeta() {
const { meta } = this.props;
if (!meta || !Array.isArray(meta))
return null;
return meta.map(
(attrs, i) => React4.createElement("meta" /* META */, {
key: i,
...toReactProps(attrs)
})
);
}
renderLink() {
const { link } = this.props;
if (!link || !Array.isArray(link))
return null;
return link.map(
(attrs, i) => React4.createElement("link" /* LINK */, {
key: i,
...toReactProps(attrs)
})
);
}
renderScript() {
const { script } = this.props;
if (!script || !Array.isArray(script))
return null;
return script.map((attrs, i) => {
const { innerHTML, ...rest } = attrs;
const props = toReactProps(rest);
if (innerHTML) {
props.dangerouslySetInnerHTML = { __html: innerHTML };
}
return React4.createElement("script" /* SCRIPT */, { key: i, ...props });
});
}
renderStyle() {
const { style } = this.props;
if (!style || !Array.isArray(style))
return null;
return style.map((attrs, i) => {
const { cssText, ...rest } = attrs;
const props = toReactProps(rest);
if (cssText) {
props.dangerouslySetInnerHTML = { __html: cssText };
}
return React4.createElement("style" /* STYLE */, { key: i, ...props });
});
}
renderNoscript() {
const { noscript } = this.props;
if (!noscript || !Array.isArray(noscript))
return null;
return noscript.map((attrs, i) => {
const { innerHTML, ...rest } = attrs;
const props = toReactProps(rest);
if (innerHTML) {
props.dangerouslySetInnerHTML = { __html: innerHTML };
}
return React4.createElement("noscript" /* NOSCRIPT */, { key: i, ...props });
});
}
render() {
return React4.createElement(
React4.Fragment,
null,
this.renderTitle(),
this.renderBase(),
this.renderMeta(),
this.renderLink(),
this.renderScript(),
this.renderStyle(),
this.renderNoscript()
);
}
};
// src/index.tsx
var Helmet = class extends Component3 {
var Helmet = class extends Component4 {
static defaultProps = {

@@ -748,3 +930,3 @@ defer: true,

let arrayTypeChildren = {};
React3.Children.forEach(children, (child) => {
React5.Children.forEach(children, (child) => {
if (!child || !child.props) {

@@ -799,3 +981,6 @@ return;

}
return helmetData ? /* @__PURE__ */ React3.createElement(HelmetDispatcher, { ...newProps, context: helmetData.value }) : /* @__PURE__ */ React3.createElement(Context.Consumer, null, (context) => /* @__PURE__ */ React3.createElement(HelmetDispatcher, { ...newProps, context }));
if (isReact19) {
return /* @__PURE__ */ React5.createElement(React19Dispatcher, { ...newProps });
}
return helmetData ? /* @__PURE__ */ React5.createElement(HelmetDispatcher, { ...newProps, context: helmetData.value }) : /* @__PURE__ */ React5.createElement(Context.Consumer, null, (context) => /* @__PURE__ */ React5.createElement(HelmetDispatcher, { ...newProps, context }));
}

@@ -802,0 +987,0 @@ };

@@ -38,3 +38,3 @@ "use strict";

module.exports = __toCommonJS(src_exports);
var import_react4 = __toESM(require("react"));
var import_react6 = __toESM(require("react"));
var import_react_fast_compare = __toESM(require("react-fast-compare"));

@@ -44,3 +44,3 @@ var import_invariant = __toESM(require("invariant"));

// src/Provider.tsx
var import_react2 = __toESM(require("react"));
var import_react3 = __toESM(require("react"));

@@ -413,4 +413,3 @@ // src/server.ts

let priorityMethods = {
toComponent: () => {
},
toComponent: () => [],
toString: () => ""

@@ -479,6 +478,11 @@ };

// src/reactVersion.ts
var import_react2 = __toESM(require("react"));
var major = parseInt(import_react2.default.version.split(".")[0], 10);
var isReact19 = major >= 19;
// src/Provider.tsx
var defaultValue = {};
var Context = import_react2.default.createContext(defaultValue);
var HelmetProvider = class _HelmetProvider extends import_react2.Component {
var Context = import_react3.default.createContext(defaultValue);
var HelmetProvider = class _HelmetProvider extends import_react3.Component {
static canUseDOM = isDocument;

@@ -488,6 +492,13 @@ helmetData;

super(props);
this.helmetData = new HelmetData(this.props.context || {}, _HelmetProvider.canUseDOM);
if (isReact19) {
this.helmetData = null;
} else {
this.helmetData = new HelmetData(this.props.context || {}, _HelmetProvider.canUseDOM);
}
}
render() {
return /* @__PURE__ */ import_react2.default.createElement(Context.Provider, { value: this.helmetData.value }, this.props.children);
if (isReact19) {
return /* @__PURE__ */ import_react3.default.createElement(import_react3.default.Fragment, null, this.props.children);
}
return /* @__PURE__ */ import_react3.default.createElement(Context.Provider, { value: this.helmetData.value }, this.props.children);
}

@@ -497,3 +508,3 @@ };

// src/Dispatcher.tsx
var import_react3 = require("react");
var import_react4 = require("react");
var import_shallowequal = __toESM(require("shallowequal"));

@@ -516,7 +527,4 @@

} else if (attribute === "cssText" /* CSS_TEXT */) {
if (newElement.styleSheet) {
newElement.styleSheet.cssText = tag.cssText;
} else {
newElement.appendChild(document.createTextNode(tag.cssText));
}
const cssText = tag.cssText;
newElement.appendChild(document.createTextNode(cssText));
} else {

@@ -644,3 +652,3 @@ const attr = attribute;

// src/Dispatcher.tsx
var HelmetDispatcher = class extends import_react3.Component {
var HelmetDispatcher = class extends import_react4.Component {
rendered = false;

@@ -663,4 +671,3 @@ shouldComponentUpdate(nextProps) {

helmetInstances.get().map((instance) => {
const props = { ...instance.props };
delete props.context;
const { context: _context, ...props } = instance.props;
return props;

@@ -694,4 +701,179 @@ })

// src/React19Dispatcher.tsx
var import_react5 = __toESM(require("react"));
var react19Instances = [];
var toHtmlAttributes = (props) => {
const result = {};
for (const key of Object.keys(props)) {
result[HTML_TAG_MAP[key] || key] = props[key];
}
return result;
};
var toReactProps = (attrs) => {
const result = {};
for (const key of Object.keys(attrs)) {
const mapped = REACT_TAG_MAP[key];
result[mapped || key] = attrs[key];
}
return result;
};
var applyAttributes = (tagName, attributes) => {
if (!isDocument)
return;
const el = document.getElementsByTagName(tagName)[0];
if (!el)
return;
const managedAttr = "data-rh-managed";
const prev = el.getAttribute(managedAttr);
const prevKeys = prev ? prev.split(",") : [];
const nextKeys = Object.keys(attributes);
for (const key of prevKeys) {
if (!nextKeys.includes(key)) {
el.removeAttribute(key);
}
}
for (const key of nextKeys) {
const value = attributes[key];
if (value === void 0 || value === null || value === false) {
el.removeAttribute(key);
} else if (value === true) {
el.setAttribute(key, "");
} else {
el.setAttribute(key, String(value));
}
}
if (nextKeys.length > 0) {
el.setAttribute(managedAttr, nextKeys.join(","));
} else {
el.removeAttribute(managedAttr);
}
};
var syncAllAttributes = () => {
const htmlAttrs = {};
const bodyAttrs = {};
for (const instance of react19Instances) {
const { htmlAttributes, bodyAttributes } = instance.props;
if (htmlAttributes) {
Object.assign(htmlAttrs, toHtmlAttributes(htmlAttributes));
}
if (bodyAttributes) {
Object.assign(bodyAttrs, toHtmlAttributes(bodyAttributes));
}
}
applyAttributes("html" /* HTML */, htmlAttrs);
applyAttributes("body" /* BODY */, bodyAttrs);
};
var React19Dispatcher = class extends import_react5.Component {
componentDidMount() {
react19Instances.push(this);
syncAllAttributes();
}
componentDidUpdate() {
syncAllAttributes();
}
componentWillUnmount() {
const index = react19Instances.indexOf(this);
if (index !== -1) {
react19Instances.splice(index, 1);
}
syncAllAttributes();
}
resolveTitle() {
const { title, titleTemplate, defaultTitle } = this.props;
if (title && titleTemplate) {
return titleTemplate.replace(/%s/g, () => Array.isArray(title) ? title.join("") : title);
}
return title || defaultTitle || void 0;
}
renderTitle() {
const title = this.resolveTitle();
if (title === void 0)
return null;
const titleAttributes = this.props.titleAttributes || {};
return import_react5.default.createElement("title" /* TITLE */, toReactProps(titleAttributes), title);
}
renderBase() {
const { base } = this.props;
if (!base)
return null;
return import_react5.default.createElement("base" /* BASE */, toReactProps(base));
}
renderMeta() {
const { meta } = this.props;
if (!meta || !Array.isArray(meta))
return null;
return meta.map(
(attrs, i) => import_react5.default.createElement("meta" /* META */, {
key: i,
...toReactProps(attrs)
})
);
}
renderLink() {
const { link } = this.props;
if (!link || !Array.isArray(link))
return null;
return link.map(
(attrs, i) => import_react5.default.createElement("link" /* LINK */, {
key: i,
...toReactProps(attrs)
})
);
}
renderScript() {
const { script } = this.props;
if (!script || !Array.isArray(script))
return null;
return script.map((attrs, i) => {
const { innerHTML, ...rest } = attrs;
const props = toReactProps(rest);
if (innerHTML) {
props.dangerouslySetInnerHTML = { __html: innerHTML };
}
return import_react5.default.createElement("script" /* SCRIPT */, { key: i, ...props });
});
}
renderStyle() {
const { style } = this.props;
if (!style || !Array.isArray(style))
return null;
return style.map((attrs, i) => {
const { cssText, ...rest } = attrs;
const props = toReactProps(rest);
if (cssText) {
props.dangerouslySetInnerHTML = { __html: cssText };
}
return import_react5.default.createElement("style" /* STYLE */, { key: i, ...props });
});
}
renderNoscript() {
const { noscript } = this.props;
if (!noscript || !Array.isArray(noscript))
return null;
return noscript.map((attrs, i) => {
const { innerHTML, ...rest } = attrs;
const props = toReactProps(rest);
if (innerHTML) {
props.dangerouslySetInnerHTML = { __html: innerHTML };
}
return import_react5.default.createElement("noscript" /* NOSCRIPT */, { key: i, ...props });
});
}
render() {
return import_react5.default.createElement(
import_react5.default.Fragment,
null,
this.renderTitle(),
this.renderBase(),
this.renderMeta(),
this.renderLink(),
this.renderScript(),
this.renderStyle(),
this.renderNoscript()
);
}
};
// src/index.tsx
var Helmet = class extends import_react4.Component {
var Helmet = class extends import_react6.Component {
static defaultProps = {

@@ -787,3 +969,3 @@ defer: true,

let arrayTypeChildren = {};
import_react4.default.Children.forEach(children, (child) => {
import_react6.default.Children.forEach(children, (child) => {
if (!child || !child.props) {

@@ -838,4 +1020,7 @@ return;

}
return helmetData ? /* @__PURE__ */ import_react4.default.createElement(HelmetDispatcher, { ...newProps, context: helmetData.value }) : /* @__PURE__ */ import_react4.default.createElement(Context.Consumer, null, (context) => /* @__PURE__ */ import_react4.default.createElement(HelmetDispatcher, { ...newProps, context }));
if (isReact19) {
return /* @__PURE__ */ import_react6.default.createElement(React19Dispatcher, { ...newProps });
}
return helmetData ? /* @__PURE__ */ import_react6.default.createElement(HelmetDispatcher, { ...newProps, context: helmetData.value }) : /* @__PURE__ */ import_react6.default.createElement(Context.Consumer, null, (context) => /* @__PURE__ */ import_react6.default.createElement(HelmetDispatcher, { ...newProps, context }));
}
};

@@ -8,3 +8,3 @@ import type { PropsWithChildren } from 'react';

context?: {
helmet?: HelmetServerState;
helmet?: HelmetServerState | null;
};

@@ -14,3 +14,3 @@ }

static canUseDOM: boolean;
helmetData: HelmetData;
helmetData: HelmetData | null;
constructor(props: PropsWithChildren<ProviderProps>);

@@ -17,0 +17,0 @@ render(): React.JSX.Element;

@@ -0,5 +1,6 @@

import React from 'react';
import type { MappedServerState } from './types';
declare const mapStateOnServer: (props: MappedServerState) => {
priority: {
toComponent: () => void;
toComponent: () => React.ReactElement[];
toString: () => string;

@@ -6,0 +7,0 @@ };

@@ -28,3 +28,3 @@ import type { HTMLAttributes, JSX } from 'react';

toString(): string;
toComponent(): React.Component<any>;
toComponent(): React.ReactElement[];
}

@@ -49,3 +49,2 @@ export interface HelmetHTMLBodyDatum {

title: HelmetDatum;
titleAttributes: HelmetDatum;
priority: HelmetDatum;

@@ -52,0 +51,0 @@ }

{
"name": "react-helmet-async",
"version": "2.0.5",
"description": "Thread-safe Helmet for React 16+ and friends",
"version": "3.0.0",
"description": "Thread-safe Helmet for React 16–18, with native support for React 19+",
"sideEffects": false,
"main": "./lib/index.js",
"module": "./lib/index.esm.js",
"typings": "./lib/index.d.ts",
"types": "./lib/index.d.ts",
"exports": {
".": {
"import": {
"types": "./lib/index.d.ts",
"default": "./lib/index.esm.js"
},
"require": {
"types": "./lib/index.d.ts",
"default": "./lib/index.js"
}
}
},
"repository": "http://github.com/staylor/react-helmet-async",

@@ -23,2 +35,3 @@ "author": "Scott Taylor <scott.c.taylor@mac.com>",

"@commitlint/config-conventional": "18.4.3",
"@playwright/test": "^1.58.2",
"@remix-run/eslint-config": "2.3.1",

@@ -31,2 +44,3 @@ "@testing-library/jest-dom": "6.1.5",

"@types/react": "18.2.39",
"@types/react-dom": "^18.3.7",
"@types/shallowequal": "1.1.5",

@@ -40,2 +54,3 @@ "@vitejs/plugin-react": "4.2.0",

"jsdom": "22.1.0",
"playwright": "^1.58.2",
"prettier": "3.1.0",

@@ -52,3 +67,3 @@ "raf": "3.4.1",

"peerDependencies": {
"react": "^16.6.0 || ^17.0.0 || ^18.0.0"
"react": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},

@@ -58,10 +73,14 @@ "scripts": {

"lint": "eslint --cache --cache-location ./node_modules/.cache/eslint --report-unused-disable-directives .",
"lint-fix": "yarn lint --fix",
"lint-fix": "pnpm lint --fix",
"test": "vitest run",
"test-watch": "yarn test --watch",
"test-update": "yarn test -u",
"compile": "yarn run clean && NODE_ENV=production tsx build.ts && yarn types",
"prepare": "yarn compile && husky install",
"types": "tsc src/index.tsx --jsx react --declaration --esModuleInterop --allowJs --emitDeclarationOnly --outDir lib"
"test:e2e:server": "vitest run --config e2e/vitest.config.ts",
"test:e2e:browser": "playwright test --config e2e/playwright.config.ts",
"test:e2e": "pnpm run test:e2e:server && pnpm run test:e2e:browser",
"test:all": "pnpm test && pnpm run test:e2e",
"test-watch": "pnpm test -- --watch",
"test-update": "pnpm test -- -u",
"compile": "pnpm run clean && NODE_ENV=production tsx build.ts && pnpm run types",
"prepare": "pnpm run compile && husky install",
"types": "tsc --project tsconfig.build.json"
}
}
# react-helmet-async
[![CircleCI](https://circleci.com/gh/staylor/react-helmet-async.svg?style=svg)](https://circleci.com/gh/staylor/react-helmet-async)
[![CI](https://github.com/staylor/react-helmet-async/actions/workflows/ci.yml/badge.svg)](https://github.com/staylor/react-helmet-async/actions/workflows/ci.yml)

@@ -12,5 +12,16 @@ [Announcement post on Times Open blog](https://open.nytimes.com/the-future-of-meta-tag-management-for-modern-react-development-ec26a7dc9183)

## React 19
React 19 has built-in support for hoisting `<title>`, `<meta>`, `<link>`, `<style>`, and `<script>` elements to `<head>`. Starting with version 3.0.0, this package detects the React version at runtime:
- **React 19+**: `<Helmet>` renders actual DOM elements and lets React handle hoisting them to `<head>`. `<HelmetProvider>` becomes a transparent passthrough. The existing API is fully compatible — you do not need to change any code.
- **React 16–18**: The existing behavior is preserved. `<Helmet>` collects all instances, deduplicates tags, and applies changes to the DOM via manual manipulation (client) or serializes them for the response (server).
> **Note:** `htmlAttributes` and `bodyAttributes` do not have a React 19 equivalent, so they are still applied via direct DOM manipulation on both code paths.
If you are starting a new React 19 project and do not need `htmlAttributes`/`bodyAttributes`, SSR `context` serialization, `onChangeClientState`, `prioritizeSeoTags`, or `titleTemplate` support, you may not need this package at all — React 19's built-in metadata handling may be sufficient.
## Usage
**New is 1.0.0:** No more default export! `import { Helmet } from 'react-helmet-async'`
**New in 1.0.0:** No more default export! `import { Helmet } from 'react-helmet-async'`

@@ -21,3 +32,3 @@ The main way that this package differs from `react-helmet` is that it requires using a Provider to encapsulate Helmet state for your React tree. If you use libraries like Redux or Apollo, you are already familiar with this paradigm:

import React from 'react';
import ReactDOM from 'react-dom';
import { createRoot } from 'react-dom/client';
import { Helmet, HelmetProvider } from 'react-helmet-async';

@@ -37,6 +48,3 @@

ReactDOM.hydrate(
app,
document.getElementById(‘app’)
);
createRoot(document.getElementById('app')).render(app);
```

@@ -74,2 +82,4 @@

> **React 19 SSR note:** When using React 19, `<title>`, `<meta>`, and `<link>` tags rendered inside `<Helmet>` are included directly in the React render output and hoisted to `<head>` by React itself. The `context` object will not be populated with helmet state on React 19. If you rely on the `context` for server rendering, you can render these tags directly in your component tree instead and let React 19 handle them natively.
## Streams

@@ -124,2 +134,4 @@

> **React 19:** React 19's `renderToReadableStream` natively handles `<title>`, `<meta>`, and `<link>` hoisting during streaming, so the manual context extraction shown above is not necessary.
## Usage in Jest

@@ -134,2 +146,4 @@ While testing in using jest, if there is a need to emulate SSR, the following string is required to have the test behave the way they are expected to.

> This is only relevant for React 16–18. On React 19, `HelmetProvider` is a passthrough and `canUseDOM` has no effect.
## Prioritizing tags for SEO

@@ -182,2 +196,4 @@

> **React 19:** The `prioritizeSeoTags` flag has no effect on React 19, since tags are rendered as regular JSX elements and their order in `<head>` is determined by React's rendering order.
## Usage without Context

@@ -190,3 +206,3 @@ You can optionally use `<Helmet>` outside a context by manually creating a stateful `HelmetData` instance, and passing that stateful object to each `<Helmet>` instance:

import { renderToString } from 'react-dom/server';
import { Helmet, HelmetProvider, HelmetData } from 'react-helmet-async';
import { Helmet, HelmetData } from 'react-helmet-async';

@@ -210,4 +226,24 @@ const helmetData = new HelmetData({});

> **React 19:** The `helmetData` prop is ignored on React 19, since `<Helmet>` renders elements directly without the need for external state management.
## Compatibility
| React Version | Behavior |
|---|---|
| 16.6+ | Full support via `HelmetProvider` context and manual DOM updates |
| 17.x | Full support via `HelmetProvider` context and manual DOM updates |
| 18.x | Full support via `HelmetProvider` context and manual DOM updates |
| 19.x+ | Renders native JSX elements; React handles `<head>` hoisting |
## Development
```bash
pnpm install
pnpm test # unit tests
pnpm run test:e2e # server + browser e2e tests
pnpm run test:all # everything
```
## License
Licensed under the Apache 2.0 License, Copyright © 2018 Scott Taylor