Reputation: 41
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 context
clientof type
functionsupplied to
Component, expected
object.
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
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