Skip to content

Commit 6a3d3bf

Browse files
authored
feat: Merge pull request #297 from jaysoo/issues/291-fouc
feat: Adds support for synchronously updated tags (Closes #291)
2 parents c947ede + b22e2f4 commit 6a3d3bf

5 files changed

+125
-32
lines changed

src/Helmet.js

+3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const Helmet = Component =>
1717
* @param {Object} base: {"target": "_blank", "href": "http://mysite.com/"}
1818
* @param {Object} bodyAttributes: {"className": "root"}
1919
* @param {String} defaultTitle: "Default Title"
20+
* @param {Boolean} defer: true
2021
* @param {Boolean} encodeSpecialCharacters: true
2122
* @param {Object} htmlAttributes: {"lang": "en", "amp": undefined}
2223
* @param {Array} link: [{"rel": "canonical", "href": "http://mysite.com/example"}]
@@ -37,6 +38,7 @@ const Helmet = Component =>
3738
PropTypes.node
3839
]),
3940
defaultTitle: PropTypes.string,
41+
defer: PropTypes.bool,
4042
encodeSpecialCharacters: PropTypes.bool,
4143
htmlAttributes: PropTypes.object,
4244
link: PropTypes.arrayOf(PropTypes.object),
@@ -51,6 +53,7 @@ const Helmet = Component =>
5153
};
5254

5355
static defaultProps = {
56+
defer: true,
5457
encodeSpecialCharacters: true
5558
};
5659

src/HelmetConstants.js

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export const REACT_TAG_MAP = {
4747

4848
export const HELMET_PROPS = {
4949
DEFAULT_TITLE: "defaultTitle",
50+
DEFER: "defer",
5051
ENCODE_SPECIAL_CHARACTERS: "encodeSpecialCharacters",
5152
ON_CHANGE_CLIENT_STATE: "onChangeClientState",
5253
TITLE_TEMPLATE: "titleTemplate"

src/HelmetUtils.js

+44-32
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ const getInnermostProperty = (propsList, property) => {
203203
const reducePropsToState = propsList => ({
204204
baseTag: getBaseTagFromPropsList([TAG_PROPERTIES.HREF], propsList),
205205
bodyAttributes: getAttributesFromPropsList(ATTRIBUTE_NAMES.BODY, propsList),
206+
defer: getInnermostProperty(propsList, HELMET_PROPS.DEFER),
206207
encode: getInnermostProperty(
207208
propsList,
208209
HELMET_PROPS.ENCODE_SPECIAL_CHARACTERS
@@ -286,6 +287,23 @@ const warn = msg => {
286287
let _helmetIdleCallback = null;
287288

288289
const handleClientStateChange = newState => {
290+
if (_helmetIdleCallback) {
291+
cancelIdleCallback(_helmetIdleCallback);
292+
}
293+
294+
if (newState.defer) {
295+
_helmetIdleCallback = requestIdleCallback(() => {
296+
commitTagChanges(newState, () => {
297+
_helmetIdleCallback = null;
298+
});
299+
});
300+
} else {
301+
commitTagChanges(newState);
302+
_helmetIdleCallback = null;
303+
}
304+
};
305+
306+
const commitTagChanges = (newState, cb) => {
289307
const {
290308
baseTag,
291309
bodyAttributes,
@@ -299,43 +317,37 @@ const handleClientStateChange = newState => {
299317
title,
300318
titleAttributes
301319
} = newState;
320+
updateAttributes(TAG_NAMES.BODY, bodyAttributes);
321+
updateAttributes(TAG_NAMES.HTML, htmlAttributes);
322+
323+
updateTitle(title, titleAttributes);
324+
325+
const tagUpdates = {
326+
baseTag: updateTags(TAG_NAMES.BASE, baseTag),
327+
linkTags: updateTags(TAG_NAMES.LINK, linkTags),
328+
metaTags: updateTags(TAG_NAMES.META, metaTags),
329+
noscriptTags: updateTags(TAG_NAMES.NOSCRIPT, noscriptTags),
330+
scriptTags: updateTags(TAG_NAMES.SCRIPT, scriptTags),
331+
styleTags: updateTags(TAG_NAMES.STYLE, styleTags)
332+
};
302333

303-
if (_helmetIdleCallback) {
304-
cancelIdleCallback(_helmetIdleCallback);
305-
}
306-
307-
_helmetIdleCallback = requestIdleCallback(() => {
308-
updateAttributes(TAG_NAMES.BODY, bodyAttributes);
309-
updateAttributes(TAG_NAMES.HTML, htmlAttributes);
310-
311-
updateTitle(title, titleAttributes);
334+
const addedTags = {};
335+
const removedTags = {};
312336

313-
const tagUpdates = {
314-
baseTag: updateTags(TAG_NAMES.BASE, baseTag),
315-
linkTags: updateTags(TAG_NAMES.LINK, linkTags),
316-
metaTags: updateTags(TAG_NAMES.META, metaTags),
317-
noscriptTags: updateTags(TAG_NAMES.NOSCRIPT, noscriptTags),
318-
scriptTags: updateTags(TAG_NAMES.SCRIPT, scriptTags),
319-
styleTags: updateTags(TAG_NAMES.STYLE, styleTags)
320-
};
337+
Object.keys(tagUpdates).forEach(tagType => {
338+
const {newTags, oldTags} = tagUpdates[tagType];
321339

322-
const addedTags = {};
323-
const removedTags = {};
324-
325-
Object.keys(tagUpdates).forEach(tagType => {
326-
const {newTags, oldTags} = tagUpdates[tagType];
340+
if (newTags.length) {
341+
addedTags[tagType] = newTags;
342+
}
343+
if (oldTags.length) {
344+
removedTags[tagType] = tagUpdates[tagType].oldTags;
345+
}
346+
});
327347

328-
if (newTags.length) {
329-
addedTags[tagType] = newTags;
330-
}
331-
if (oldTags.length) {
332-
removedTags[tagType] = tagUpdates[tagType].oldTags;
333-
}
334-
});
348+
cb && cb();
335349

336-
_helmetIdleCallback = null;
337-
onChangeClientState(newState, addedTags, removedTags);
338-
});
350+
onChangeClientState(newState, addedTags, removedTags);
339351
};
340352

341353
const flattenArray = possibleArray => {

test/HelmetDeclarativeTest.js

+36
Original file line numberDiff line numberDiff line change
@@ -2501,6 +2501,42 @@ describe("Helmet - Declarative API", () => {
25012501
});
25022502
});
25032503

2504+
describe("deferred tags", () => {
2505+
beforeEach(() => {
2506+
window.__spy__ = sinon.spy();
2507+
});
2508+
2509+
afterEach(() => {
2510+
delete window.__spy__;
2511+
});
2512+
2513+
it("executes synchronously when defer={true} and async otherwise", done => {
2514+
ReactDOM.render(
2515+
<div>
2516+
<Helmet defer={false}>
2517+
<script>
2518+
window.__spy__(1)
2519+
</script>
2520+
</Helmet>
2521+
<Helmet>
2522+
<script>
2523+
window.__spy__(2)
2524+
</script>
2525+
</Helmet>
2526+
</div>,
2527+
container
2528+
);
2529+
2530+
expect(window.__spy__.callCount).to.equal(1);
2531+
2532+
requestIdleCallback(() => {
2533+
expect(window.__spy__.callCount).to.equal(2);
2534+
expect(window.__spy__.args).to.deep.equal([[1], [2]]);
2535+
done();
2536+
});
2537+
});
2538+
});
2539+
25042540
describe("server", () => {
25052541
const stringifiedHtmlAttributes = `lang="ga" class="myClassName"`;
25062542
const stringifiedBodyAttributes = `lang="ga" class="myClassName"`;

test/HelmetTest.js

+41
Original file line numberDiff line numberDiff line change
@@ -2271,6 +2271,47 @@ describe("Helmet", () => {
22712271
});
22722272
});
22732273

2274+
describe("deferred tags", () => {
2275+
beforeEach(() => {
2276+
window.__spy__ = sinon.spy();
2277+
});
2278+
2279+
afterEach(() => {
2280+
delete window.__spy__;
2281+
});
2282+
2283+
it("executes synchronously when defer={true} and async otherwise", done => {
2284+
ReactDOM.render(
2285+
<div>
2286+
<Helmet
2287+
defer={false}
2288+
script={[
2289+
{
2290+
innerHTML: `window.__spy__(1)`
2291+
}
2292+
]}
2293+
/>
2294+
<Helmet
2295+
script={[
2296+
{
2297+
innerHTML: `window.__spy__(2)`
2298+
}
2299+
]}
2300+
/>
2301+
</div>,
2302+
container
2303+
);
2304+
2305+
expect(window.__spy__.callCount).to.equal(1);
2306+
2307+
requestIdleCallback(() => {
2308+
expect(window.__spy__.callCount).to.equal(2);
2309+
expect(window.__spy__.args).to.deep.equal([[1], [2]]);
2310+
done();
2311+
});
2312+
});
2313+
});
2314+
22742315
describe("server", () => {
22752316
const stringifiedHtmlAttributes = `lang="ga" class="myClassName"`;
22762317
const stringifiedTitle = `<title ${HELMET_ATTRIBUTE}="true">Dangerous &lt;script&gt; include</title>`;

0 commit comments

Comments
 (0)