diff --git a/src/Helmet.js b/src/Helmet.js index 38c61b6e..d88ee7e4 100644 --- a/src/Helmet.js +++ b/src/Helmet.js @@ -17,6 +17,7 @@ const Helmet = Component => * @param {Object} base: {"target": "_blank", "href": "http://mysite.com/"} * @param {Object} bodyAttributes: {"className": "root"} * @param {String} defaultTitle: "Default Title" + * @param {Boolean} defer: true * @param {Boolean} encodeSpecialCharacters: true * @param {Object} htmlAttributes: {"lang": "en", "amp": undefined} * @param {Array} link: [{"rel": "canonical", "href": "http://mysite.com/example"}] @@ -37,6 +38,7 @@ const Helmet = Component => PropTypes.node ]), defaultTitle: PropTypes.string, + defer: PropTypes.bool, encodeSpecialCharacters: PropTypes.bool, htmlAttributes: PropTypes.object, link: PropTypes.arrayOf(PropTypes.object), @@ -51,6 +53,7 @@ const Helmet = Component => }; static defaultProps = { + defer: true, encodeSpecialCharacters: true }; diff --git a/src/HelmetConstants.js b/src/HelmetConstants.js index 1aaf8817..37818463 100644 --- a/src/HelmetConstants.js +++ b/src/HelmetConstants.js @@ -47,6 +47,7 @@ export const REACT_TAG_MAP = { export const HELMET_PROPS = { DEFAULT_TITLE: "defaultTitle", + DEFER: "defer", ENCODE_SPECIAL_CHARACTERS: "encodeSpecialCharacters", ON_CHANGE_CLIENT_STATE: "onChangeClientState", TITLE_TEMPLATE: "titleTemplate" diff --git a/src/HelmetUtils.js b/src/HelmetUtils.js index 3abf32fe..2c79f99c 100644 --- a/src/HelmetUtils.js +++ b/src/HelmetUtils.js @@ -203,6 +203,7 @@ const getInnermostProperty = (propsList, property) => { const reducePropsToState = propsList => ({ baseTag: getBaseTagFromPropsList([TAG_PROPERTIES.HREF], propsList), bodyAttributes: getAttributesFromPropsList(ATTRIBUTE_NAMES.BODY, propsList), + defer: getInnermostProperty(propsList, HELMET_PROPS.DEFER), encode: getInnermostProperty( propsList, HELMET_PROPS.ENCODE_SPECIAL_CHARACTERS @@ -286,6 +287,23 @@ const warn = msg => { let _helmetIdleCallback = null; const handleClientStateChange = newState => { + if (_helmetIdleCallback) { + cancelIdleCallback(_helmetIdleCallback); + } + + if (newState.defer) { + _helmetIdleCallback = requestIdleCallback(() => { + commitTagChanges(newState, () => { + _helmetIdleCallback = null; + }); + }); + } else { + commitTagChanges(newState); + _helmetIdleCallback = null; + } +}; + +const commitTagChanges = (newState, cb) => { const { baseTag, bodyAttributes, @@ -299,43 +317,37 @@ const handleClientStateChange = newState => { title, titleAttributes } = newState; + updateAttributes(TAG_NAMES.BODY, bodyAttributes); + updateAttributes(TAG_NAMES.HTML, htmlAttributes); + + updateTitle(title, titleAttributes); + + const tagUpdates = { + baseTag: updateTags(TAG_NAMES.BASE, baseTag), + linkTags: updateTags(TAG_NAMES.LINK, linkTags), + metaTags: updateTags(TAG_NAMES.META, metaTags), + noscriptTags: updateTags(TAG_NAMES.NOSCRIPT, noscriptTags), + scriptTags: updateTags(TAG_NAMES.SCRIPT, scriptTags), + styleTags: updateTags(TAG_NAMES.STYLE, styleTags) + }; - if (_helmetIdleCallback) { - cancelIdleCallback(_helmetIdleCallback); - } - - _helmetIdleCallback = requestIdleCallback(() => { - updateAttributes(TAG_NAMES.BODY, bodyAttributes); - updateAttributes(TAG_NAMES.HTML, htmlAttributes); - - updateTitle(title, titleAttributes); + const addedTags = {}; + const removedTags = {}; - const tagUpdates = { - baseTag: updateTags(TAG_NAMES.BASE, baseTag), - linkTags: updateTags(TAG_NAMES.LINK, linkTags), - metaTags: updateTags(TAG_NAMES.META, metaTags), - noscriptTags: updateTags(TAG_NAMES.NOSCRIPT, noscriptTags), - scriptTags: updateTags(TAG_NAMES.SCRIPT, scriptTags), - styleTags: updateTags(TAG_NAMES.STYLE, styleTags) - }; + Object.keys(tagUpdates).forEach(tagType => { + const {newTags, oldTags} = tagUpdates[tagType]; - const addedTags = {}; - const removedTags = {}; - - Object.keys(tagUpdates).forEach(tagType => { - const {newTags, oldTags} = tagUpdates[tagType]; + if (newTags.length) { + addedTags[tagType] = newTags; + } + if (oldTags.length) { + removedTags[tagType] = tagUpdates[tagType].oldTags; + } + }); - if (newTags.length) { - addedTags[tagType] = newTags; - } - if (oldTags.length) { - removedTags[tagType] = tagUpdates[tagType].oldTags; - } - }); + cb && cb(); - _helmetIdleCallback = null; - onChangeClientState(newState, addedTags, removedTags); - }); + onChangeClientState(newState, addedTags, removedTags); }; const flattenArray = possibleArray => { diff --git a/test/HelmetDeclarativeTest.js b/test/HelmetDeclarativeTest.js index d7c3bd0b..aa290be4 100644 --- a/test/HelmetDeclarativeTest.js +++ b/test/HelmetDeclarativeTest.js @@ -2501,6 +2501,42 @@ describe("Helmet - Declarative API", () => { }); }); + describe("deferred tags", () => { + beforeEach(() => { + window.__spy__ = sinon.spy(); + }); + + afterEach(() => { + delete window.__spy__; + }); + + it("executes synchronously when defer={true} and async otherwise", done => { + ReactDOM.render( +