diff --git a/CHANGELOG.md b/CHANGELOG.md index f5035abc3..37042d237 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### Changed + +- De-duplicate identical `' + ) + + const Helmet = loadHelmet() + + // This is the hydration case: the component that declared the script + // mounts on the client, but the browser already ran the server-rendered + // tag, so forwarding it to react-helmet would make it execute twice. + render() + + expect(renderedScriptSrcs()).toEqual([]) +}) + +it('drops a server-rendered script src declared as a JSX child', () => { + givenServerRenderedHead( + '' + ) + + const Helmet = loadHelmet() + + render( + + ' + ) + + loadHelmet() + + // Without `data-react-helmet`, react-helmet no longer recognizes the tag + // as its own, so it won't remove it during its first DOM sync -- the tag + // stays exactly where the server put it. + expect(document.head.querySelectorAll('script[src]')).toHaveLength(1) + expect( + document.head.querySelectorAll('script[src][data-react-helmet]') + ).toHaveLength(0) +}) + +it('only adopts server-rendered scripts that have a src', () => { + givenServerRenderedHead( + '' + ) + + const Helmet = loadHelmet() + + render() + + expect(renderedScriptSrcs()).toEqual(['https://example.com/a.js']) +}) + +it('does not adopt scripts that react-helmet did not render', () => { + givenServerRenderedHead('') + + const Helmet = loadHelmet() + + // A script the page put in the head by itself isn't react-helmet's to + // manage, so react-helmet won't remove/re-add it and there's nothing to + // de-duplicate against. + render() + + expect(renderedScriptSrcs()).toEqual(['https://example.com/a.js']) +}) + +it('still forwards scripts that were not server-rendered', () => { + givenServerRenderedHead( + '' + ) + + const Helmet = loadHelmet() + + render( + + ) + + expect(renderedScriptSrcs()).toEqual(['https://example.com/b.js']) +}) + +it('starts each fresh module instance with an empty registry', () => { + const FirstHelmet = loadHelmet() + + render() + expect(renderedScriptSrcs()).toEqual(['https://example.com/a.js']) + cleanup() + + // Sanity check for the test helper itself: a fresh module (as if the + // page had reloaded) must not remember the previous instance's src. + const SecondHelmet = loadHelmet() + + render() + expect(renderedScriptSrcs()).toEqual(['https://example.com/a.js']) +}) diff --git a/react/components/Helmet.tsx b/react/components/Helmet.tsx index 94063efb5..6162204b2 100644 --- a/react/components/Helmet.tsx +++ b/react/components/Helmet.tsx @@ -1,4 +1,194 @@ -import { Helmet } from 'react-helmet' +// NOTE: using plain (non-`import type`) imports for the type-only names +// below (`ReactNode`, `HelmetProps`) is intentional -- the project's +// installed `babel-preset-react-app` (7.0.2) doesn't support the +// `import type { ... }` syntax and jest fails to parse it. +import React, { Children, isValidElement, ReactNode } from 'react' +import { Helmet as ReactHelmet, HelmetProps } from 'react-helmet' + +/** + * `