Next.js: Relay Modern example

Created on 19 Apr 2017  路  14Comments  路  Source: vercel/next.js

With the announcement of Relay Modern, it would be great to assemble an example of Next.js + Relay. According to this @josephsavona's comment it should be possible to have SSR.

example

Most helpful comment

@Gregoor @josephsavona - As a short term solution to server side rendering for relay modern specifically in next.js... (WIP, don't judge the code too much).

I have a HOC called NextPage that does some generic setup for all our pages:

import React, { Component } from 'react';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import materialTheme from '../lib/materialTheme';
import environment from '../lib/relayEnvironment';


export default query => ComposedComponent => class NextPage extends Component {
  static async getInitialProps(ctx) {
    const { req } = ctx;
    const isServer = !!req;
    const userAgent = req ? req.headers['user-agent'] : navigator.userAgent;

    let pageProps = {};

    if (query) {
      const {
        createOperationSelector,
        getOperation,
      } = environment.unstable_internal;
      const operation = createOperationSelector(getOperation(query));
      const result = await new Promise((resolve) => environment.streamQuery({
        operation,
        onNext: () => resolve(environment.lookup(operation.fragment).data),
      }));

      pageProps = {
        ...result,
      };

      if (isServer) pageProps.recordSource = environment.getStore().getSource().toJSON();
    }

    if (ComposedComponent.getInitialProps) {
      pageProps = {
        ...await ComposedComponent.getInitialProps(ctx),
      };
    }

    return {
      ...pageProps,
      // initialState: store.getState(),
      isServer,
      userAgent,
    };
  }

  render () {
    const { userAgent } = this.props;
    return (
      <MuiThemeProvider muiTheme={getMuiTheme(materialTheme, { userAgent })}>
        <ComposedComponent {...this.props} />
      </MuiThemeProvider>
    )
  }
};

Then in my page,

import React, { Component } from 'react';
import { QueryRenderer, graphql } from 'react-relay';
import environment from '../lib/relayEnvironment';
import NextPage from '../decorators/NextPage';
import Layout from '../containers/Layout';

const query = graphql`
  query pagesQuery {
    root {
      properties {
        edges {
          node {
            id
            propertyName
            street
          }
        }
      }
    }
  }
`

class HomePage extends Component {
  render() {
    return (
      <QueryRenderer
        environment={environment}
        query={query}
        render={({error, props}) => {
          console.log(error, props);
          if (error) {
            return <Layout>{error.message}</Layout>;
          } else if (props || this.props) {
            const serProps = props || this.props;
            return (
              <Layout>
                {serProps.root.properties.edges.map(({ node }) => <p>{node.propertyName}</p>)}
              </Layout>
            );
          }
          return <Layout>Loading</Layout>;
        }}
      />
    );
  }
}

export default NextPage(query)(HomePage);

This is definitely a workaround, as we are essentially hijacking the render method of QueryRenderer to render the response from getInitialProps instead of props inside the QueryRenderer scope. This workaround is potentially alleviated by https://github.com/facebook/relay/pull/1760 which will add a lookup prop to QueryRenderer. Also you'll see in my HOC if we are on the server i'm adding recordSource: to the returned props, which will get added into the __NEXT_DATA__ and then i use the value from that to hydrate the store like so:

let initial = {};

// Hydrate the store from __NEXT_DATA__
if (typeof window !== 'undefined' && window.__NEXT_DATA__) {
  initial = window.__NEXT_DATA__.recordSource;
}

const source = new RecordSource(initial);
const store = new Store(source);

All 14 comments

I continued working on it in the repo I created when I was trying to get Relay-Antique to work with Next: https://github.com/Gregoor/next.js-relay-example

Unfortunately I'm currently stuck with this issue: https://github.com/facebook/relay/issues/1631
Will continue working on it, as soon as that is resolved.

That particular issue is solved for now, but it's still not there yet. Haven't quite figured out how to get the Environment to accept my hydration. @josephsavona's comment about the possibility of SSR seems more like a description of a better future, as of right now react-relay is still using an unstable_internal API from relay-runtime (/core). So that's what I had to do as well.

@Gregoor Hydration of the environment is straightforward but not documented: the RecordSource object has a toJSON() method, the result of which can be passed back to the constructor to rehydrate.

The larger blocker for SSR is that by default the QueryRenderer and network layer do not read from cache and always effectively force-fetch. i would take a look at how QueryRenderer works, and create a variation that supports SSR.

@Gregoor @josephsavona - As a short term solution to server side rendering for relay modern specifically in next.js... (WIP, don't judge the code too much).

I have a HOC called NextPage that does some generic setup for all our pages:

import React, { Component } from 'react';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import materialTheme from '../lib/materialTheme';
import environment from '../lib/relayEnvironment';


export default query => ComposedComponent => class NextPage extends Component {
  static async getInitialProps(ctx) {
    const { req } = ctx;
    const isServer = !!req;
    const userAgent = req ? req.headers['user-agent'] : navigator.userAgent;

    let pageProps = {};

    if (query) {
      const {
        createOperationSelector,
        getOperation,
      } = environment.unstable_internal;
      const operation = createOperationSelector(getOperation(query));
      const result = await new Promise((resolve) => environment.streamQuery({
        operation,
        onNext: () => resolve(environment.lookup(operation.fragment).data),
      }));

      pageProps = {
        ...result,
      };

      if (isServer) pageProps.recordSource = environment.getStore().getSource().toJSON();
    }

    if (ComposedComponent.getInitialProps) {
      pageProps = {
        ...await ComposedComponent.getInitialProps(ctx),
      };
    }

    return {
      ...pageProps,
      // initialState: store.getState(),
      isServer,
      userAgent,
    };
  }

  render () {
    const { userAgent } = this.props;
    return (
      <MuiThemeProvider muiTheme={getMuiTheme(materialTheme, { userAgent })}>
        <ComposedComponent {...this.props} />
      </MuiThemeProvider>
    )
  }
};

Then in my page,

import React, { Component } from 'react';
import { QueryRenderer, graphql } from 'react-relay';
import environment from '../lib/relayEnvironment';
import NextPage from '../decorators/NextPage';
import Layout from '../containers/Layout';

const query = graphql`
  query pagesQuery {
    root {
      properties {
        edges {
          node {
            id
            propertyName
            street
          }
        }
      }
    }
  }
`

class HomePage extends Component {
  render() {
    return (
      <QueryRenderer
        environment={environment}
        query={query}
        render={({error, props}) => {
          console.log(error, props);
          if (error) {
            return <Layout>{error.message}</Layout>;
          } else if (props || this.props) {
            const serProps = props || this.props;
            return (
              <Layout>
                {serProps.root.properties.edges.map(({ node }) => <p>{node.propertyName}</p>)}
              </Layout>
            );
          }
          return <Layout>Loading</Layout>;
        }}
      />
    );
  }
}

export default NextPage(query)(HomePage);

This is definitely a workaround, as we are essentially hijacking the render method of QueryRenderer to render the response from getInitialProps instead of props inside the QueryRenderer scope. This workaround is potentially alleviated by https://github.com/facebook/relay/pull/1760 which will add a lookup prop to QueryRenderer. Also you'll see in my HOC if we are on the server i'm adding recordSource: to the returned props, which will get added into the __NEXT_DATA__ and then i use the value from that to hydrate the store like so:

let initial = {};

// Hydrate the store from __NEXT_DATA__
if (typeof window !== 'undefined' && window.__NEXT_DATA__) {
  initial = window.__NEXT_DATA__.recordSource;
}

const source = new RecordSource(initial);
const store = new Store(source);

Hey @brad-decker, thanks for posting that. Unfortunately I'm not getting it to server-render something. If you have the time, could you check if I went wrong somewhere? Here is the repo:
https://github.com/Gregoor/next.js-relay-example

If anyone is looking for working solution https://github.com/este/este/blob/d959fb96dac5a3e346a2897ae4f2e81a9948ac17/components/app.js

Note we are fetching query even for page transition. Next.js + Relay Modern = 馃樆 Finally, after many years, the developers can write JS apps without annoying spinners everywhere. This is the panacea.

As for an implementation. It's based on https://github.com/robrichard/relay-context-provider Thank you! Note it does not support subscriptions, but I think subscriptions don't belong into page query.

Great work @steida 馃憤 I've prepared a simple example based on your solution. Comments are welcome 馃檪

Unfortunately it doesn't work offline :(

Can someone close the issue please?

@steida sure thing 馃憣

Hi @timneutkens , I'm getting this error "Can't load ./cmds plugin:Error: Cannot find module 'graphql-playground/middleware'" with with-relay-modern example. Any idea what's going on?

Hi, @astenmies I'm not aware that this example would depend on GraphQL Playground. Anyway, there must have been some changes in this package recently. If you are using it in your Express server, this should fix it:

dependencies: {
  "graphql-playground-middleware-express": "^1.1.2",
}

and this:

const playground = require('graphql-playground-middleware-express').default
...
app.use('/playground', playground({ endpoint: '/graphql' }))

anyone already looking into gettings this to work with React 16? @petrvlcek maybe?

Edit: Okay, update was trivial. Created a PR for it #3365

Was this page helpful?
0 / 5 - 0 ratings

Related issues

knipferrc picture knipferrc  路  3Comments

havefive picture havefive  路  3Comments

kenji4569 picture kenji4569  路  3Comments

YarivGilad picture YarivGilad  路  3Comments

irrigator picture irrigator  路  3Comments