Skip to content

Commit

Permalink
Use postMessage to reduce latency, support older browsers
Browse files Browse the repository at this point in the history
This commit makes a few tweaks to support older browsers and updates
the code transition process to use window.postMessage. This avoids
loading React on every single change.
  • Loading branch information
nhunzaker committed Sep 6, 2018
1 parent 3b057a2 commit d3da7b9
Show file tree
Hide file tree
Showing 3 changed files with 184 additions and 127 deletions.
117 changes: 1 addition & 116 deletions fixtures/dom/public/renderer.html
Original file line number Diff line number Diff line change
Expand Up @@ -79,121 +79,6 @@

<div id="output"></div>

<script>
'use strict';

(function() {
var output = document.getElementById('output');
var status = document.getElementById('status');
var hydrate = document.getElementById('hydrate');
var reload = document.getElementById('reload');
var renders = 0;
var failed = false;

function query(key) {
let pattern = new RegExp(key + '=([^&]+)(&|$)');
let matches = window.location.search.match(pattern);

if (matches) {
return decodeURIComponent(matches[1]);
}

handleError(new Error('No key found for' + key));
}

function booleanQuery(key) {
return query(key) === 'true'
}

function setStatus(label) {
status.innerHTML = label;
}

function prerender() {
setStatus('Generating markup');

output.innerHTML = ReactDOMServer.renderToString(
React.createElement(Fixture)
);

setStatus('Markup only (No React)');
}

function render() {
setStatus('Hydrating');

if (ReactDOM.hydrate) {
ReactDOM.hydrate(React.createElement(Fixture), output);
} else {
ReactDOM.render(React.createElement(Fixture), output);
}

setStatus(renders > 0 ? 'Re-rendered (' + renders + 'x)' : 'Hydrated');
renders += 1;
hydrate.innerHTML = 'Re-render';
}

function handleError (error) {
console.log(error)
failed = true;
setStatus('Javascript Error');
output.innerHTML = error
};

function loadScript(src) {
return new Promise(function (resolve, reject) {
var script = document.createElement('script');
script.async = 1;
script.src = src;

script.onload = resolve;
script.onerror = function(error) {
reject(new Error('Unable to load ' + src));
};

document.body.appendChild(script);
})
}

function injectScript(src) {
return new Promise(function (resolve, reject) {
var script = document.createElement('script');
script.textContent = src;
document.body.appendChild(script);
})
}

window.onerror = handleError

reload.onclick = function() {
window.location.reload();
};

hydrate.onclick = render;

Promise.all([
loadScript(query('reactPath')),
booleanQuery('needsReactDOM') ? loadScript(query('reactDOMPath')) : null,
loadScript(query('reactDOMServerPath'))
]).then(function () {
injectScript(query('code'))

if (failed) {
return;
}

if (typeof Fixture === 'undefined') {
setStatus('Failed');
output.innerHTML = 'Please name your root component "Fixture"';
} else {
prerender();

if (booleanQuery('hydrate')) {
render();
}
}
}).catch(handleError);
})();
</script>
<script src="renderer.js"></script>
</body>
</html>
138 changes: 138 additions & 0 deletions fixtures/dom/public/renderer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/**
* Supports render.html, a piece of the hydration fixture. See /hydration
*/

'use strict';

(function() {
var output = document.getElementById('output');
var status = document.getElementById('status');
var hydrate = document.getElementById('hydrate');
var reload = document.getElementById('reload');
var renders = 0;
var failed = false;

function query(key) {
var pattern = new RegExp(key + '=([^&]+)(&|$)');
var matches = window.location.search.match(pattern);

if (matches) {
return decodeURIComponent(matches[1]);
}

handleError(new Error('No key found for' + key));
}

function booleanQuery(key) {
return query(key) === 'true';
}

function setStatus(label) {
status.innerHTML = label;
}

function prerender() {
setStatus('Generating markup');

output.innerHTML = ReactDOMServer.renderToString(
React.createElement(Fixture)
);

setStatus('Markup only (No React)');
}

function render() {
setStatus('Hydrating');

if (ReactDOM.hydrate) {
ReactDOM.hydrate(React.createElement(Fixture), output);
} else {
ReactDOM.render(React.createElement(Fixture), output);
}

setStatus(renders > 0 ? 'Re-rendered (' + renders + 'x)' : 'Hydrated');
renders += 1;
hydrate.innerHTML = 'Re-render';
}

function handleError(error) {
console.log(error);
failed = true;
setStatus('Javascript Error');
output.innerHTML = error;
}

function loadScript(src) {
return new Promise(function(resolve, reject) {
var script = document.createElement('script');
script.async = true;
script.src = src;

script.onload = resolve;
script.onerror = function(error) {
reject(new Error('Unable to load ' + src));
};

document.body.appendChild(script);
});
}

function injectFixture(src) {
var script = document.createElement('script');
script.textContent = src;
document.body.appendChild(script);

if (typeof Fixture === 'undefined') {
setStatus('Failed');
output.innerHTML = 'Please name your root component "Fixture"';
} else {
prerender();

if (booleanQuery('hydrate')) {
render();
}
}
}

function reloadFixture(code) {
renders = 0;
ReactDOM.unmountComponentAtNode(output);
injectFixture(code);
}

window.onerror = handleError;

reload.onclick = function() {
window.location.reload();
};

hydrate.onclick = render;

loadScript(query('reactPath'))
.then(function() {
return booleanQuery('needsReactDOM')
? loadScript(query('reactDOMPath'))
: null;
})
.then(function() {
return loadScript(query('reactDOMServerPath'));
})
.then(function() {
if (failed) {
return;
}

window.addEventListener('message', function(event) {
switch (event.data.type) {
case 'code':
reloadFixture(event.data.payload);
break;
default:
throw new Error('Unrecognized message: ' + event.data.type);
}
});

window.parent.postMessage({type: 'ready'}, '*');
})
.catch(handleError);
})();
56 changes: 45 additions & 11 deletions fixtures/dom/src/components/fixtures/hydration/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,58 @@ class Hydration extends React.Component {
hydrate: true,
};

ready = false;

componentDidMount() {
window.addEventListener('message', this.handleMessage);
}

componentWillUnmount() {
window.removeEventListener('message', this.handleMessage);
}

injectCode = () => {
this.send({
type: 'code',
payload: buble.transform(this.state.code).code,
});
};

setFrame = frame => {
this.frame = frame;
};

handleMessage = event => {
switch (event.data.type) {
case 'ready':
this.ready = true;
this.injectCode();
break;
default:
throw new Error('Unrecognized message: ' + event.data.type);
// do nothing
}
};

send = message => {
if (this.ready) {
this.frame.contentWindow.postMessage(message, '*');
}
};

setCode = code => {
this.setState({code});
this.setState({code}, this.injectCode);
};

setCheckbox = event => {
const {name, checked} = event.target;
this.setState({[name]: checked});
this.setState({
[event.target.name]: event.target.checked,
});
};

render() {
const {code, hydrate} = this.state;

const src =
'/renderer.html?' +
qs.stringify({
code: buble.transform(code).code,
hydrate,
...reactPaths(),
});
const src = '/renderer.html?' + qs.stringify({hydrate, ...reactPaths()});

return (
<LiveProvider code={code}>
Expand All @@ -55,6 +88,7 @@ class Hydration extends React.Component {
</div>
</section>
<iframe
ref={this.setFrame}
className="hydration-frame"
title="Hydration Preview"
src={src}
Expand Down

0 comments on commit d3da7b9

Please sign in to comment.