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