relay-tools / react-relay-network-modern-ssr

SSR middleware for react-relay-network-modern
https://github.com/relay-tools/react-relay-network-modern-ssr
MIT License
67 stars 10 forks source link

Relay store is not updated during server side rendering #1

Closed brad-decker closed 1 year ago

brad-decker commented 6 years ago

When it comes to priming the cache of react-relay-network-modern, this middleware does a fantastic job. But what i'm running into currently is that nothing is actually rendered server side because the relay store is populated AFTER .getCache() which happens AFTER renderToString or whatever SSR implementation there is. I'm using next.js.

The result of this:

<QueryRenderer
        query={query}
        variables={vars}
        environment={relayEnv}
        render={({ props, error }) => {
          if (error) return error;
          if (!props) return 'loading';
          return <OtherComp data={props.data} />;
        }}
      />

is that the html sent to the client is 'loading...' -- of course once the middlware kicks in and client side query returns immediately from cache it renders quickly but its not really rendering what is expected on the server.

Thoughts?

nodkz commented 6 years ago

I do not know how it can be solved with Next.js.

In my app I'm using double renderToString call:

It works quite fast and does not force me to make any hacks with ReactRouter4.

HsuTing commented 5 years ago

I found a solution, I add two files:

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

let storeEnvironment = null;

export const { relaySSR, environment, createEnvironment, } = (isBrowser => { if (isBrowser) { const RelaySSR = require('react-relay-network-modern-ssr/node8/client').default;

return {
  createEnvironment: relayData => {
    if (storeEnvironment) return storeEnvironment;

    const relaySSR = new RelaySSR(relayData);

    storeEnvironment = new Environment({
      store,
      network: new RelayNetworkLayer([
        cacheMiddleware({
          size: 100,
          ttl: 60 * 1000,
        }),
        relaySSR.getMiddleware({
          lookup: false,
        }), 
      ]), 
    }); 

    return storeEnvironment;
  }, 
};  

}

const RelaySSR = require('react-relay-network-modern-ssr/node8/server').default; const relaySSR = new RelaySSR();

return { relaySSR, environment: new Environment({ store, network: new RelayNetworkLayer([ relaySSR.getMiddleware(), ]), }), createEnvironment: relayData => new Environment({ store, network: Network.create(() => relayData[0][1]), }), }; })(ExecutionEnvironment.canUseDOM);


- withData
```js
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import { QueryRenderer } from 'react-relay';

import { environment, createEnvironment, relaySSR } from './createEnvironment';

export default (Component, { query, variables }) => class WithData extends React.PureComponent {
  static displayName = `WithData(${Component.displayName})`;

  static getInitialProps = async ctx => {
    ReactDOMServer.renderToString(
      <QueryRenderer
        environment={environment}
        query={query}
        variables={variables}
        render={() => null}
      />
    );  

    return {
      ...(await Component.getInitialProps?.(ctx) || {}), // use for next.js
      relayData: await relaySSR.getCache(),
    };  
  }

  render() {
    const { relayData, ...componentProps } = this.props;

    return (
      <QueryRenderer
        environment={createEnvironment(relayData)}
        query={query}
        variables={variables}
        render={({ error, props }) => {
          if (error)
            return <div>{error.message}</div>;
          else if (props)
            return (
              <Component
                {...componentProps}
                {...props}
              />
            );

          return <div>Loading</div>;
        }}
      />
    );
  }
};

And you can use like:

class Test extends React.PureComponent {
  render() {
    return (
      <div>...</div>
    );
  }
}

export default withData(Test, {
  query: graphql`
    query user {
      name
    }
  `,
  variables: {
    key: 'value',
  },
});

I think the result why you can not use react-relay-network-modern-ssr in next.js is that react does not handle async/await in the component. I had printed the response in react-relay-network-modern-ssr, and I found the data is in the cache. The response printed after the component render. As a result, react-relay-network-modern-ssr did not work.

I add the simple network which does not need to wait async/await. This environment give data to QueryRenderer directly. So, it can work in the server side.

HsuTing commented 5 years ago

This is the version that can work in _app.js with next.js, and this does not use withData.

import { environment, createEnvironment, relaySSR, } from './createEnvironment';

export default class App extends NextApp { static getInitialProps = async ({ Component, router, ctx }) => { const { variables } = (await Component.getInitialProps?.(ctx)) || {};

if (environment)
  ReactDOMServer.renderToString(
    <QueryRenderer
      environment={environment}
      query={Component.query}
      variables={variables}
      render={() => null}
    />,
  );  

return {
  variables,
  relayData: await relaySSR?.getCache(),
};

};

render() { const { Component, variables = {}, relayData } = this.props;

return (
  <Container>
    <QueryRenderer
      environment={createEnvironment(relayData)}
      query={Component.query}
      variables={variables}
      render={({ error, props }) => {
        if (error) return <div>{error.message}</div>;
        else if (props) return <Component {...props} />;
        return <div>Loading</div>;
      }}
    />
  </Container>
);

} }


and write page like this:
```js
export default class Test extends React.PureComponent {
  static getInitialProps = async () => ({
    variables: {
      key: 'value',
    },
  });

  static query = graphql`
    query user {
      name
    }
  `;

  render() {
    return (
      <div>...</div>
    );
  }
}

Hope to help you 😸 .

ScripterSugar commented 5 years ago

@HsuTing Your solution did really good help for me! Thanks for sharing :)

HsuTing commented 5 years ago

@WDFS Thank you ^^ I had added an example in next.js, if someone need a example project, you can see here.

d3vhound commented 5 years ago

@HsuTing that example actually doesn't work.

HsuTing commented 5 years ago

@d3vhound Did you get any error?

d3vhound commented 5 years ago

https://github.com/zeit/next.js/issues/7565

HsuTing commented 5 years ago

You can try to downgrade react-relay to 1.5.0. After I tested, I think this is something wrong when using react-relay-network-modern with react-relay >= 1.6.0.

HsuTing commented 5 years ago

@d3vhound I fix the example here.

stan-sack commented 5 years ago

@HsuTing I'm using your example and it works perfectly now. One issue I'm having though is that the server initially renders the page with stale cached data before fetching the fresh data over the network on the client. Is there any way I can disable this cache and force the server to render the page with network data? Something similar to dataFrom={"NETWORK_ONLY"} in QueryRenderer?

HsuTing commented 5 years ago

@stan-sack Sorry, we can not do it, now. React SSR can not use async/await. This is the reason why we get the data and set the data in cache before rendering. As a result, when we render the component, we do not request the data from the network and we can get data in QueryRenderer. If we do not use cache, QueryRenderer will need to wait the request working. However, React will not block the rendering before request done. As a result, you will never get the data in SSR. That is why we can not use QueryRenderer with SSR directly.

The another important thing is that we do not use the cache in QueryRenderer. In react-relay-network-modern-ssr, we make the cache in react-relay-network-modern-ssr, not QueryRenderer, and return the cache when QueryRenderer request the data from the network. The default value of dataForm is NETWORK_ONLY in QueryRenderer.

On the other hand, React is available to use lazy loading. However, it can only use in the client side. After lazy loading is available to use in the server side one day, we will not need to to cache the data before rendering. You can see here to learn the more information.

stan-sack commented 5 years ago

Ah. Thankyou for the explanation!

ODelibalta commented 4 years ago

I think the result why you can not use react-relay-network-modern-ssr in next.js is that react does not handle async/await in the component. I had printed the response in react-relay-network-modern-ssr, and I found the data is in the cache. The response printed after the component render. As a result, react-relay-network-modern-ssr did not work.

Is that why the react-relay-network-modern-ssr example repo in nextjs is taken off? I started a project couple of weeks ago and the repo was there. Now I am planning to start a new one that I will actually work on but I am not sure if I should just copy my package.json from the other project or not.

HsuTing commented 4 years ago

Yes. If you have any problem about react-relay-network-modern-ssr in next.js, I think you can try to fix this or open a issue in next.js.

snrbrnjna commented 4 years ago

pretty sure, that the problem here is solved with this PR https://github.com/facebook/relay/pull/2883