Relay Modern SSR
// @flow
import React, { Component } from "react";
import PropTypes from "prop-types";
import App from "./components/App";
type Props = {
environment: *,
queryTracker?: *
};
export default class Root extends Component<Props> {
render() {
return (
<App environment={this.props.environment} />
);
}
getChildContext() {
return {
queryTracker: this.props.queryTracker
};
}
static childContextTypes = {
queryTracker: PropTypes.object
};
}
// @flow
import React, { PureComponent, type ElementProps } from "react";
import PropTypes from "prop-types";
import { QueryRenderer } from "react-relay";
type Props = ElementProps<typeof QueryRenderer>;
type Context = {
queryTracker?: *
};
export default (process.browser
? class TrackedQueryRenderer extends PureComponent<Props> {
render() {
return <QueryRenderer dataFrom="STORE_THEN_NETWORK" {...this.props} />;
}
}
: class TrackedQueryRenderer extends PureComponent<Props> {
context: Context;
constructor(props: Props, context: Context) {
super(props, context);
if (this.context.queryTracker) this.context.queryTracker.add(this);
}
componentWillUnmount() {
if (this.context.queryTracker) this.context.queryTracker.delete(this);
}
render() {
const queryTracker = this.context.queryTracker;
if (!queryTracker) return <QueryRenderer {...this.props} />;
return (
<QueryRenderer
{...this.props}
render={(result: *) => {
if (result.props || result.error) {
queryTracker.delete(this);
} else {
queryTracker.add(this);
}
return this.props.render(result);
}}
/>
);
}
static contextTypes = {
queryTracker: PropTypes.object
};
});
// @flow
import React from "react";
import serialize from "serialize-javascript";
import Root from "./Root";
import createRelayEnvironment from "./relay";
import { render } from "react-dom";
class Tracker<T> {
set: Set<T>;
onSettle: () => void;
constructor(onSettle: () => void) {
this.set = new Set();
this.onSettle = onSettle;
}
add(value: T) {
return this.set.add(value);
}
delete(value: T) {
const result = this.set.delete(value);
if (result && !this.set.size) {
setImmediate(this.onSettle);
}
return result;
}
}
// Render our root element into a string.
const origin = window.API;
const app: Promise<*> = new Promise((resolve, reject) => {
var i = 3;
const { environment } = createRelayEnvironment(origin);
const tracker = new Tracker(() => {
// we've reached our max iterations
if (i-- === 0) {
console.warn(`Max iterations reached: ${window.location.href}`);
return resolve(environment);
}
if (!tracker.set.size) resolve(environment);
});
// Render our root element into the DOM.
const rootEl = document.getElementById("root");
render(<Root environment={environment} queryTracker={tracker} />, rootEl);
}).then(environment => {
// create a script tag with the
const script = document.createElement("script");
script.type = "text/javascript";
script.appendChild(
document.createTextNode(
"window.__PRELOADED_RELAY_STATE = " +
serialize(
environment
.getStore()
.getSource()
.toJSON()
)
)
);
// append the script tag to the body
if (document.body) document.body.appendChild(script);
});
export default app;
const { JSDOM, CookieJar, VirtualConsole } = require("jsdom");
const { Cookie } = require("tough-cookie");
const fetch = require("node-fetch");
module.exports = async function render(template, script, ctx, contentType) {
try {
const API = process.env.API;
if (!API) {
throw new Error(
'Environment variable "API" must be defined for SSR to work.'
);
}
// cookies
const cookieJar = new CookieJar();
const deviceId = ctx.cookies.get("DEVICE", { signed: false });
if (deviceId)
cookieJar.setCookieSync(
Cookie.parse(`DEVICE=${encodeURIComponent(deviceId)}`),
ctx.request.origin
);
const sessionId = ctx.cookies.get("SESSION", { signed: false });
if (sessionId)
cookieJar.setCookieSync(
Cookie.parse(`SESSION=${encodeURIComponent(sessionId)}`),
ctx.request.origin
);
// console
const virtualConsole = new VirtualConsole();
virtualConsole.sendTo(console);
const dom = new JSDOM(template, {
contentType,
url: ctx.request.href,
referrer: ctx.request.header.referer,
userAgent: ctx.request.header["user-agent"],
cookieJar,
virtualConsole,
runScripts: "outside-only",
pretendToBeVisual: true,
beforeParse(window) {
window.closed = false;
window.fetch = fetch;
window.module = {};
window.URL.createObjectURL = () => "";
window.sessionStorage = {};
window.API = API;
}
});
const { body, status } = await dom.runVMScript(script).default;
ctx.contentType = contentType;
ctx.status = status || 200;
ctx.body = body;
} catch (err) {
console.error(err);
ctx.status = 500;
ctx.body = template;
}
};