Skip to content

Commit

Permalink
Adds boolean defer prop to <Helmet> that controls whether updates are…
Browse files Browse the repository at this point in the history
… sync or deferred. Default is defer={true}. Closes #291
  • Loading branch information
jaysoo committed Jun 23, 2017
1 parent c947ede commit b22e2f4
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 32 deletions.
3 changes: 3 additions & 0 deletions src/Helmet.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"}]
Expand All @@ -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),
Expand All @@ -51,6 +53,7 @@ const Helmet = Component =>
};

static defaultProps = {
defer: true,
encodeSpecialCharacters: true
};

Expand Down
1 change: 1 addition & 0 deletions src/HelmetConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
76 changes: 44 additions & 32 deletions src/HelmetUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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 => {
Expand Down
36 changes: 36 additions & 0 deletions test/HelmetDeclarativeTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<div>
<Helmet defer={false}>
<script>
window.__spy__(1)
</script>
</Helmet>
<Helmet>
<script>
window.__spy__(2)
</script>
</Helmet>
</div>,
container
);

expect(window.__spy__.callCount).to.equal(1);

requestIdleCallback(() => {
expect(window.__spy__.callCount).to.equal(2);
expect(window.__spy__.args).to.deep.equal([[1], [2]]);
done();
});
});
});

describe("server", () => {
const stringifiedHtmlAttributes = `lang="ga" class="myClassName"`;
const stringifiedBodyAttributes = `lang="ga" class="myClassName"`;
Expand Down
41 changes: 41 additions & 0 deletions test/HelmetTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -2271,6 +2271,47 @@ describe("Helmet", () => {
});
});

describe("deferred tags", () => {
beforeEach(() => {
window.__spy__ = sinon.spy();
});

afterEach(() => {
delete window.__spy__;
});

it("executes synchronously when defer={true} and async otherwise", done => {
ReactDOM.render(
<div>
<Helmet
defer={false}
script={[
{
innerHTML: `window.__spy__(1)`
}
]}
/>
<Helmet
script={[
{
innerHTML: `window.__spy__(2)`
}
]}
/>
</div>,
container
);

expect(window.__spy__.callCount).to.equal(1);

requestIdleCallback(() => {
expect(window.__spy__.callCount).to.equal(2);
expect(window.__spy__.args).to.deep.equal([[1], [2]]);
done();
});
});
});

describe("server", () => {
const stringifiedHtmlAttributes = `lang="ga" class="myClassName"`;
const stringifiedTitle = `<title ${HELMET_ATTRIBUTE}="true">Dangerous &lt;script&gt; include</title>`;
Expand Down

0 comments on commit b22e2f4

Please sign in to comment.