Geoff
Geoff

Reputation: 41

Prevent client side re-render when using SSR and Apollo client

Problem in a nutshell is I server side render an html doc then the React app hydrates and re-renders what is already there. After that point the app works client side just great.
I am using React, Apollo Client (Boost 0.3.1) , Node, Express, and a graphql server we have in house.

See this in action here: https://www.slowdownshow.org/

Mostly I have tried what is suggested in the docs: https://www.apollographql.com/docs/react/features/server-side-rendering

Here is what is not clear. Am I to assume that if I implement Store Rehydration the Apollo Client xhr request to fetch the data will not need to happen? If so the problem is I've tried what the docs suggest for store rehydration, but the doc is a little ambiguous

    <script>
        window.__APOLLO_STATE__ = JSON.stringify(client.extract());
    </script>

What is client in this case? I believe it is the ApolloClient. But it is a method not an object, if I use that here I get error messages like

Warning: Failed context type: Invalid contextclientof typefunctionsupplied toComponent, expectedobject.

If the Store Rehydration technique is not the way to prevent unnecessary client side re-renders - it's not clear to me what is.

Here is the relevant server code:

    import React from 'react';
    import ReactDOM from 'react-dom/server';
    import { ApolloProvider, renderToStringWithData } from 'react-apollo';
    import { ApolloClient } from 'apollo-client';
    import { createHttpLink } from 'apollo-link-http';
    import { InMemoryCache } from 'apollo-cache-inmemory';
    import FragmentMatcher from '../shared/graphql/FragmentMatcher';
    import { HelmetProvider } from 'react-helmet-async';
    import { ServerLocation } from 'apm-titan';
    import App from '../shared/App';
    import fs from 'fs';
    import os from 'os';
    import {
      globalHostFunc,
      replaceTemplateStrings,
      isFresh,
      apm_etag,
      siteConfigFunc
    } from './utils';

    export default function ReactAppSsr(app) {
      app.use((req, res) => {
        const helmetContext = {};
        const filepath =
          process.env.APP_PATH === 'relative' ? 'build' : 'current/build';
        const forwarded = globalHostFunc(req).split(':')[0];
        const siteConfig = siteConfigFunc(forwarded);
        const hostname = os.hostname();
        const context = {};
        const cache = new InMemoryCache({ fragmentMatcher: FragmentMatcher });
        let graphqlEnv = hostname.match(/dev/) ? '-dev' : '';
        graphqlEnv = process.env.NODE_ENV === 'development' ? '-dev' : graphqlEnv;
        const graphqlClient = (graphqlEnv) => {
          return new ApolloClient({
            ssrMode: false,
            cache,
            link: createHttpLink({
              uri: `https://xxx${graphqlEnv}.xxx.org/api/v1/graphql`,
              fetch: fetch
            })
          });
        };
        let template = fs.readFileSync(`${filepath}/index.html`).toString();
        const component = (
          <ApolloProvider client={graphqlClient}>
            <HelmetProvider context={helmetContext}>
              <ServerLocation url={req.url} context={context}>
                <App forward={forwarded} />
              </ServerLocation>
            </HelmetProvider>
          </ApolloProvider>
        );
        renderToStringWithData(component).then(() => {
          const { helmet } = helmetContext;
          let str = ReactDOM.renderToString(component);
          const is404 = str.match(/Not Found\. 404/);
          if (is404?.length > 0) {
            str = 'Not Found 404.';
            template = replaceTemplateStrings(template, '', '', '', '');
            res.status(404);
            res.send(template);
            return;
          }
          template = replaceTemplateStrings(
            template,
            helmet.title.toString(),
            helmet.meta.toString(),
            helmet.link.toString(),
            str
          );
          template = template.replace(/__GTMID__/g, `${siteConfig.gtm}`);
          const apollo_state = ` <script>
               window.__APOLLO_STATE__ = JSON.stringify(${graphqlClient.extract()});
            </script>
          </body>`;
          template = template.replace(/<\/body>/, apollo_state);
          res.set('Cache-Control', 'public, max-age=120');
          res.set('ETag', apm_etag(str));
          if (isFresh(req, res)) {
            res.status(304);
            res.send();
            return;
          }
          res.send(template);
          res.status(200);
        });
      });
    }

client side:

    import App from '../shared/App';
    import React from 'react';
    import { hydrate } from 'react-dom';
    import { ApolloProvider } from 'react-apollo';
    import { HelmetProvider } from 'react-helmet-async';
    import { client } from '../shared/graphql/graphqlClient';
    import '@babel/polyfill';

    const graphqlEnv = window.location.href.match(/local|dev/) ? '-dev' : '';

    const graphqlClient = client(graphqlEnv);

    const Wrapped = () => {
      const helmetContext = {};
      return (
        <HelmetProvider context={helmetContext}>
          <ApolloProvider client={graphqlClient}>
            <App />
          </ApolloProvider>
        </HelmetProvider>
      );
    };

    hydrate(<Wrapped />, document.getElementById('root'));

    if (module.hot) {
      module.hot.accept();
    }

graphqlCLinet.js:

    import fetch from 'cross-fetch';
    import { ApolloClient } from 'apollo-client';
    import { createHttpLink } from 'apollo-link-http';
    import { InMemoryCache } from 'apollo-cache-inmemory';
    import FragmentMatcher from './FragmentMatcher';

    const cache = new InMemoryCache({ fragmentMatcher: FragmentMatcher });

    export const client = (graphqlEnv) => {
      return new ApolloClient({
        ssrMode: true,
        cache,
        link: createHttpLink({
          uri: `https://xxx${graphqlEnv}.xxx.org/api/v1/graphql`,
          fetch: fetch
        })
      });
    };

FragmentMatcher.js:

    import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';

    const FragmentMatcher = new IntrospectionFragmentMatcher({
      introspectionQueryResultData: {
        __schema: {
          types: [
            {
              kind: 'INTERFACE',
              name: 'resourceType',
              possibleTypes: [
                { name: 'Episode' },
                { name: 'Link' },
                { name: 'Page' },
                { name: 'Profile' },
                { name: 'Story' }
              ]
            }
          ]
        }
      }
    });

    export default FragmentMatcher;

See client side re-renders in action
https://www.slowdownshow.org/

In the production version of the code above, I skip state rehydration window.__APOLLO_STATE__ = JSON.stringify(${graphqlClient.extract()}); as I do not have it working

Upvotes: 1

Views: 2642

Answers (1)

Geoff
Geoff

Reputation: 41

So the answer was simple once I realized I was making a mistake. I needed to put

        window.__APOLLO_STATE__ = JSON.stringify(client.extract());
    </script>

BEFORE everything else so it could be read and used.

This const apollo_state = ` <script> window.__APOLLO_STATE__ = JSON.stringify(${graphqlClient.extract()}); </script> </body>`; template = template.replace(/<\/body>/, apollo_state);

needed to go up by the <head> not down by the body. Such a no duh now but tripped me up for a while

Upvotes: 1

Related Questions