sibelius
3/7/2018 - 5:46 PM

Relay Modern SSR

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;
  }
};