My component currently hydrates on the browser, something I'd love to avoid. When you visit the link, I want it to come pre-hydrated with all the data it needs to display, i.e. rendered on the server. Currently, the component looks like this:
import { graphql } from "react-apollo";
import gql from 'graphql-tag';
import withData from "../../apollo/with-data";
import getPostsQuery from '../../apollo/schemas/getPostsQuery.graphql';
const renderers = {
paragraph: (props) => <Typography variant="body2" gutterBottom {...props} />,
};
const GET_POSTS = gql`${getPostsQuery}`;
const PostList = ({data: {error, loading, posts}}) => {
let payload;
if(error) {
payload = (<div>There was an error!</div>);
} else if(loading) {
payload = (<div>Loading...</div>);
} else {
payload = (
<>
{posts.map((post) => (
<div>
<div>{post.title}</div>
<div>{post.body}</div>
</div>
))}
</>
);
}
return payload;
};
export default withData(graphql(GET_POSTS)(PostList));
As you can see, it displays the text Loading...
at first as it fetches the posts in the background. I don't want that. I want it to already come pre-hydrated with the fetched data.
For reference, my Apollo initializations look like this:
// apollo/with-data.js
import React from "react";
import PropTypes from "prop-types";
import { ApolloProvider, getDataFromTree } from "react-apollo";
import initApollo from "./init-apollo";
export default ComposedComponent => {
return class WithData extends React.Component {
static displayName = `WithData(${ComposedComponent.displayName})`;
static propTypes = {
serverState: PropTypes.object.isRequired
};
static async getInitialProps(ctx) {
const headers = ctx.req ? ctx.req.headers : {};
let serverState = {};
// Evaluate the composed component's getInitialProps()
let composedInitialProps = {};
if (ComposedComponent.getInitialProps) {
composedInitialProps = await ComposedComponent.getInitialProps(ctx);
}
// Run all graphql queries in the component tree
// and extract the resulting data
if (!process.browser) {
const apollo = initApollo(headers);
// Provide the `url` prop data in case a graphql query uses it
const url = { query: ctx.query, pathname: ctx.pathname };
// Run all graphql queries
const app = (
<ApolloProvider client={apollo}>
<ComposedComponent url={url} {...composedInitialProps} />
</ApolloProvider>
);
await getDataFromTree(app);
// Extract query data from the Apollo's store
const state = apollo.getInitialState();
serverState = {
apollo: {
// Make sure to only include Apollo's data state
data: state.data
}
};
}
return {
serverState,
headers,
...composedInitialProps
};
}
constructor(props) {
super(props);
this.apollo = initApollo(this.props.headers, this.props.serverState);
}
render() {
return (
<ApolloProvider client={this.apollo}>
<ComposedComponent {...this.props} />
</ApolloProvider>
);
}
};
};
// apollo/init-apollo.js
import { InMemoryCache } from 'apollo-cache-inmemory';
import { ApolloClient } from 'apollo-client';
import { ApolloLink } from 'apollo-link';
import { onError } from 'apollo-link-error';
import { HttpLink } from 'apollo-link-http';
import fetch from 'isomorphic-fetch';
let apolloClient = null;
// Polyfill fetch() on the server (used by apollo-client)
if (!process.browser) {
global.fetch = fetch;
}
const create = (headers, initialState) => new ApolloClient({
initialState,
link: ApolloLink.from([
onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
graphQLErrors.forEach(({ message, locations, path }) => console.log(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`,
));
}
if (networkError) console.log(`[Network error]: ${networkError}`);
}),
new HttpLink({
// uri: 'https://dev.schandillia.com/graphql',
uri: process.env.CMS,
credentials: 'same-origin',
}),
]),
ssrMode: !process.browser, // Disables forceFetch on the server (so queries are only run once)
cache: new InMemoryCache(),
});
export default function initApollo(headers, initialState = {}) {
// Make sure to create a new client for every server-side request so that data
// isn't shared between connections (which would be bad)
if (!process.browser) {
return create(headers, initialState);
}
// Reuse client on the client-side
if (!apolloClient) {
apolloClient = create(headers, initialState);
}
return apolloClient;
}
UPDATE: I tried incorporating the official withApollo example at https://github.com/zeit/next.js/tree/canary/examples/with-apollo into my project but it throws an invariant error on getDataFromTree()
:
Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined.
I used the exact same code as in the example repo, for the /init/apollo.js
, /components/blog/PostList.jsx
, and /pages/Blog/jsx
files. The only difference in my specific case is that I have an explicit _app.jsx
that reads as follows:
/* eslint-disable max-len */
import '../static/styles/fonts.scss';
import '../static/styles/style.scss';
import '../static/styles/some.css';
import CssBaseline from '@material-ui/core/CssBaseline';
import { ThemeProvider } from '@material-ui/styles';
import jwt from 'jsonwebtoken';
import withRedux from 'next-redux-wrapper';
import App, {
Container,
} from 'next/app';
import Head from 'next/head';
import React from 'react';
import { Provider } from 'react-redux';
import makeStore from '../reducers';
import mainTheme from '../themes/main-theme';
import getSessIDFromCookies from '../utils/get-sessid-from-cookies';
import getLanguageFromCookies from '../utils/get-language-from-cookies';
import getUserTokenFromCookies from '../utils/get-user-token-from-cookies';
import removeFbHash from '../utils/remove-fb-hash';
class MyApp extends App {
static async getInitialProps({ Component, ctx }) {
let userToken;
let sessID;
let language;
if (ctx.isServer) {
ctx.store.dispatch({ type: 'UPDATEIP', payload: ctx.req.headers['x-real-ip'] });
userToken = getUserTokenFromCookies(ctx.req);
sessID = getSessIDFromCookies(ctx.req);
language = getLanguageFromCookies(ctx.req);
const dictionary = require(`../dictionaries/${language}`);
ctx.store.dispatch({ type: 'SETLANGUAGE', payload: dictionary });
if(ctx.res) {
if(ctx.res.locals) {
if(!ctx.res.locals.authenticated) {
userToken = null;
sessID = null;
}
}
}
if (userToken && sessID) { // TBD: validate integrity of sessID
const userInfo = jwt.verify(userToken, process.env.JWT_SECRET);
ctx.store.dispatch({ type: 'ADDUSERINFO', payload: userInfo });
}
ctx.store.dispatch({ type: 'ADDSESSION', payload: sessID }); // component will be able to read from store's state when rendered
}
const pageProps = Component.getInitialProps ? await Component.getInitialProps(ctx) : {};
return { pageProps };
}
componentDidMount() {
// Remove the server-side injected CSS.
const jssStyles = document.querySelector('#jss-server-side');
if (jssStyles) {
jssStyles.parentNode.removeChild(jssStyles);
}
// Register serviceWorker
if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/serviceWorker.js'); }
// Handle FB's ugly redirect URL hash
removeFbHash(window, document);
}
render() {
const { Component, pageProps, store } = this.props;
return (
<Container>
<Head>
<meta name="viewport" content="user-scalable=0, initial-scale=1, minimum-scale=1, width=device-width, height=device-height, shrink-to-fit=no" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="194x194" href="/favicon-194x194.png" />
<link rel="icon" type="image/png" sizes="192x192" href="/android-chrome-192x192.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#663300" />
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="msapplication-TileImage" content="/mstile-144x144.png" />
</Head>
<ThemeProvider theme={mainTheme}>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
<Provider store={store}>
<Component {...pageProps} />
</Provider>
</ThemeProvider>
</Container>
);
}
}
export default withRedux(makeStore)(MyApp);
Getting rid of this file is not an option as this is where I'm handling some pre-load cookie logic.
The repo, for reference, is up at https://github.com/amitschandillia/proost/tree/master/web
There are 2 key things you want to achieve when using Next.js and Apollo: SSR and Cached network data. It is a difficult balance to achieve both. But it is possible.
The way to do it is:
Now if you have some page data that you need to edit, add or delete and you want the page to update after your changes without refreshing the page, above is not enough. Because if for instance, you edit data, the typical/recommended Apollo approach is not to do anything. Apollo handles it all magically for you. Except the initial data must have come from the apollo cache and it must have an Id field. Now as you have loaded the initial data directly from the server, it is most likely, you did not read the data from a previously cached data.
So Step 2 below is then required to enable the auto-refresh of data on data changes.
This way, you continue to use all your favourite and the latest tools such as useQuery and getDataFromTree without much ado.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With