Shaleen Jain

· 2 min read

Prefetching Data with Next.js and Apollo

<blockquote> <p>Update: My pull requests implementing the techniques discussed here were merged into the zeit/next.js <a rel="noopener nofollow noreferrer external" target="_blank" href="https://github.com/zeit/next.js/tree/canary/examples/with-data-prefetch">with-data-prefetch</a> and <a rel="noopener nofollow noreferrer external" target="_blank" href="https://github.com/zeit/next.js/tree/canary/examples/with-apollo">with-apollo</a> examples. You can easily combine the various examples (including the above two) to achieve your desired functionality.</p> </blockquote> <p>For most modern web apps, network speed and latency is still the biggest constraint for high performance and great UX. Universally rendered Isomorphic applications do a great job to solve that to some extent without sacrificing interactivity by processing the complete DOM of the first request on the server and subsequent requests on the client. But there is a lot more we can do.</p> <span id="continue-reading"></span> <p>Modern web apps already have their core application logic on the client by the time the first request completes. From there on anticipating the user action and selectively prefetching the data can lead to great performance and UX improvements.</p> <p><a rel="noopener nofollow noreferrer external" target="_blank" href="https://www.apollographql.com/client/">Apollo</a> and <a rel="noopener nofollow noreferrer external" target="_blank" href="https://github.com/zeit/next.js">Next</a> is the stack of our choice, having the necessary capabilities, to demonstrate our concept.</p> <p>Adam Soffer’s <a rel="noopener nofollow noreferrer external" target="_blank" href="https://dev-blog.apollodata.com/whats-next-js-for-apollo-e4dfe835d070">introduction</a> summarizes <a rel="noopener nofollow noreferrer external" target="_blank" href="https://www.apollographql.com/client/">Apollo</a> and <a rel="noopener nofollow noreferrer external" target="_blank" href="https://github.com/zeit/next.js">Next</a> with their concerns perfectly:</p> <blockquote> <p>Apollo is a GraphQL client that’s concerned exclusively with the data layer of our application; it cares about efficiently fetching our data. Next, on the other hand, is a minimalistic framework for server-rendered React applications that’s concerned exclusively with the UI layer of our application; it cares about efficiently rendering our UI.</p> </blockquote> <h2 id="tying-together-next-js-and-apollo"><a class="zola-anchor" href="#tying-together-next-js-and-apollo" aria-label="Anchor link for: tying-together-next-js-and-apollo">🔗</a> Tying together Next.js and Apollo</h2> <p>Next has a unique way of loading data, every page has a lifecycle hook function <code>getInitialProps</code> which is called while loading on the client as well as the server. This is a very nice separation of our data logic from the UI logic and is what enables us to use the full power of Apollo and Next as both of these frameworks make no assumptions about each others layer and provides the perfect abstraction to connects these two layers.</p> <p>Apollo client’s react library <a rel="noopener nofollow noreferrer external" target="_blank" href="https://github.com/apollographql/react-apollo">react-apollo</a> provides a helper function <code>getDataFromTree</code> which recursively traverses the supplied React component and executes every <code>graphql()</code> query it finds, storing the result in its local cache (configured by us).</p> <p>The excellent <a rel="noopener nofollow noreferrer external" target="_blank" href="https://github.com/zeit/next.js/tree/canary/examples/with-apollo">example</a> from the Next.js repo shows how we can combine these two functionalities by creating a HOC which wraps <code>getDataFromTree</code> within <code>getInitialProps</code> and returning the cached data as a plain Javascript Object</p> <p>Here we make sure to use <code>getDataFromTree</code> on both the client and for SSR, altering the example like so:</p> <pre class="giallo" style="color: #657B83; background-color: #FDF6E3;" ><code data-lang="javascript"><span class="giallo-l"><span style="color: #859900;">import</span><span style="color: #268BD2;"> React</span><span style="color: #859900;"> from</span><span style="color: #2AA198;"> &#39;react&#39;</span></span> <span class="giallo-l"><span style="color: #859900;">import</span><span style="color: #268BD2;"> PropTypes</span><span style="color: #859900;"> from</span><span style="color: #2AA198;"> &#39;prop-types&#39;</span></span> <span class="giallo-l"><span style="color: #859900;">import</span><span> {</span><span style="color: #268BD2;"> ApolloProvider</span><span>,</span><span style="color: #268BD2;"> getDataFromTree</span><span> }</span><span style="color: #859900;"> from</span><span style="color: #2AA198;"> &#39;react-apollo&#39;</span></span> <span class="giallo-l"><span style="color: #859900;">import</span><span style="color: #268BD2;"> Head</span><span style="color: #859900;"> from</span><span style="color: #2AA198;"> &#39;next/head&#39;</span></span> <span class="giallo-l"></span> <span class="giallo-l"><span style="color: #859900;">import</span><span style="color: #268BD2;"> initApollo</span><span style="color: #859900;"> from</span><span style="color: #2AA198;"> &#39;./initApollo&#39;</span></span> <span class="giallo-l"></span> <span class="giallo-l"><span style="color: #859900;">export default</span><span> ComposedComponent</span><span style="color: #586E75;font-weight: bold;"> =&gt;</span><span> {</span></span> <span class="giallo-l"><span style="color: #859900;"> return</span><span style="color: #586E75;font-weight: bold;"> class</span><span style="color: #CB4B16;"> WithData</span><span style="color: #586E75;font-weight: bold;"> extends</span><span style="color: #CB4B16;"> React</span><span>.</span><span style="color: #6C71C4;">Component</span><span> {</span></span> <span class="giallo-l"><span style="color: #586E75;font-weight: bold;"> static</span><span> displayName</span><span style="color: #859900;"> =</span><span style="color: #2AA198;"> `WithData(${</span><span style="color: #268BD2;">getComponentDisplayName</span><span>(</span></span> <span class="giallo-l"><span style="color: #268BD2;"> ComposedComponent</span></span> <span class="giallo-l"><span> )</span><span style="color: #2AA198;">})`</span></span> <span class="giallo-l"><span style="color: #586E75;font-weight: bold;"> static</span><span> propTypes</span><span style="color: #859900;"> =</span><span> {</span></span> <span class="giallo-l"><span> serverState:</span><span style="color: #268BD2;"> PropTypes</span><span>.</span><span style="color: #268BD2;">object</span><span>.</span><span style="color: #268BD2;">isRequired</span></span> <span class="giallo-l"><span> }</span></span> <span class="giallo-l"></span> <span class="giallo-l"><span style="color: #586E75;font-weight: bold;"> static async</span><span style="color: #268BD2;"> getInitialProps</span><span>(ctx) {</span></span> <span class="giallo-l"><span style="color: #93A1A1;font-style: italic;"> // Initial serverState with apollo (empty)</span></span> <span class="giallo-l"><span style="color: #586E75;font-weight: bold;"> let</span><span style="color: #268BD2;"> serverState</span></span> <span class="giallo-l"></span> <span class="giallo-l"><span style="color: #93A1A1;font-style: italic;"> // Evaluate the composed component&#39;s getInitialProps()</span></span> <span class="giallo-l"><span style="color: #586E75;font-weight: bold;"> let</span><span style="color: #268BD2;"> composedInitialProps</span><span style="color: #859900;"> =</span><span> {}</span></span> <span class="giallo-l"><span style="color: #859900;"> if</span><span> (</span><span style="color: #268BD2;">ComposedComponent</span><span>.</span><span style="color: #268BD2;">getInitialProps</span><span>) {</span></span> <span class="giallo-l"><span style="color: #268BD2;"> composedInitialProps</span><span style="color: #859900;"> = await</span><span style="color: #268BD2;"> ComposedComponent</span><span>.</span><span style="color: #268BD2;">getInitialProps</span><span>(</span><span style="color: #268BD2;">ctx</span><span>)</span></span> <span class="giallo-l"><span> }</span></span> <span class="giallo-l"></span> <span class="giallo-l"><span style="color: #93A1A1;font-style: italic;"> // Run all GraphQL queries in the component tree</span></span> <span class="giallo-l"><span style="color: #93A1A1;font-style: italic;"> // and extract the resulting data</span></span> <span class="giallo-l"><span style="color: #586E75;font-weight: bold;"> const</span><span style="color: #268BD2;"> apollo</span><span style="color: #859900;"> =</span><span style="color: #268BD2;"> initApollo</span><span>();</span></span> <span class="giallo-l"></span> <span class="giallo-l"><span style="color: #859900;"> try</span><span> {</span></span> <span class="giallo-l"><span style="color: #93A1A1;font-style: italic;"> // Run all GraphQL queries</span></span> <span class="giallo-l"><span style="color: #859900;"> await</span><span style="color: #268BD2;"> getDataFromTree</span><span>(</span></span> <span class="giallo-l"><span style="color: #93A1A1;"> &lt;</span><span style="color: #859900;">ComposedComponent</span><span style="color: #93A1A1;"> ctx</span><span style="color: #859900;">=</span><span style="color: #DC322F;">{</span><span style="color: #268BD2;">ctx</span><span style="color: #DC322F;">} {</span><span style="color: #859900;">...</span><span style="color: #268BD2;">composedInitialProps</span><span style="color: #DC322F;">}</span><span style="color: #93A1A1;"> /&gt;</span><span>,</span></span> <span class="giallo-l"><span> {</span></span> <span class="giallo-l"><span> router: {</span></span> <span class="giallo-l"><span> asPath:</span><span style="color: #268BD2;"> ctx</span><span>.</span><span style="color: #268BD2;">asPath</span><span>,</span></span> <span class="giallo-l"><span> pathname:</span><span style="color: #268BD2;"> ctx</span><span>.</span><span style="color: #268BD2;">pathname</span><span>,</span></span> <span class="giallo-l"><span> query:</span><span style="color: #268BD2;"> ctx</span><span>.</span><span style="color: #268BD2;">query</span></span> <span class="giallo-l"><span> },</span></span> <span class="giallo-l"><span> client:</span><span style="color: #268BD2;"> apollo</span><span>,</span></span> <span class="giallo-l"><span> }</span></span> <span class="giallo-l"><span> )</span></span> <span class="giallo-l"><span> }</span><span style="color: #859900;"> catch</span><span> (</span><span style="color: #268BD2;">error</span><span>) {</span></span> <span class="giallo-l"><span style="color: #93A1A1;font-style: italic;"> // Prevent Apollo Client GraphQL errors from crashing SSR.</span></span> <span class="giallo-l"><span style="color: #93A1A1;font-style: italic;"> // Handle them in components via the data.error prop:</span></span> <span class="giallo-l"><span style="color: #93A1A1;font-style: italic;"> // https://www.apollographql.com/docs/react/basics/queries.html#graphql-query-data-error</span></span> <span class="giallo-l"><span> }</span></span> <span class="giallo-l"></span> <span class="giallo-l"><span style="color: #859900;"> if</span><span> (</span><span style="color: #859900;">!</span><span style="color: #268BD2;">process</span><span>.</span><span style="color: #268BD2;">browser</span><span>) {</span></span> <span class="giallo-l"><span style="color: #93A1A1;font-style: italic;"> // getDataFromTree does not call componentWillUnmount</span></span> <span class="giallo-l"><span style="color: #93A1A1;font-style: italic;"> // head side effect therefore need to be cleared manually</span></span> <span class="giallo-l"><span style="color: #268BD2;"> Head</span><span>.</span><span style="color: #268BD2;">rewind</span><span>()</span></span> <span class="giallo-l"><span> }</span></span> <span class="giallo-l"></span> <span class="giallo-l"><span style="color: #93A1A1;font-style: italic;"> // Extract query data from the Apollo store</span></span> <span class="giallo-l"><span style="color: #268BD2;"> serverState</span><span style="color: #859900;"> =</span><span> {</span></span> <span class="giallo-l"><span> apollo: {</span></span> <span class="giallo-l"><span> data:</span><span style="color: #268BD2;"> apollo</span><span>.</span><span style="color: #268BD2;">cache</span><span>.</span><span style="color: #268BD2;">extract</span><span>()</span></span> <span class="giallo-l"><span> }</span></span> <span class="giallo-l"><span> }</span></span> <span class="giallo-l"></span> <span class="giallo-l"><span style="color: #859900;"> return</span><span> {</span></span> <span class="giallo-l"><span style="color: #268BD2;"> serverState</span><span>,</span></span> <span class="giallo-l"><span style="color: #859900;"> ...</span><span style="color: #268BD2;">composedInitialProps</span></span> <span class="giallo-l"><span> }</span></span> <span class="giallo-l"><span> }</span></span> <span class="giallo-l"></span> <span class="giallo-l"><span style="color: #586E75;font-weight: bold;"> constructor</span><span>(props) {</span></span> <span class="giallo-l"><span style="color: #268BD2;"> super</span><span>(</span><span style="color: #268BD2;">props</span><span>)</span></span> <span class="giallo-l"><span style="color: #268BD2;"> this</span><span>.</span><span style="color: #268BD2;">apollo</span><span style="color: #859900;"> =</span><span style="color: #268BD2;"> initApollo</span><span>(</span><span style="color: #268BD2;">this</span><span>.</span><span style="color: #268BD2;">props</span><span>.</span><span style="color: #268BD2;">serverState</span><span>.</span><span style="color: #268BD2;">apollo</span><span>.</span><span style="color: #268BD2;">data</span><span>)</span></span> <span class="giallo-l"><span> }</span></span> <span class="giallo-l"></span> <span class="giallo-l"><span style="color: #268BD2;"> render</span><span>() {</span></span> <span class="giallo-l"><span style="color: #859900;"> return</span><span> (</span></span> <span class="giallo-l"><span style="color: #93A1A1;"> &lt;</span><span style="color: #859900;">ApolloProvider</span><span style="color: #93A1A1;"> client</span><span style="color: #859900;">=</span><span style="color: #DC322F;">{</span><span style="color: #268BD2;">this</span><span>.</span><span style="color: #268BD2;">apollo</span><span style="color: #DC322F;">}</span><span style="color: #93A1A1;">&gt;</span></span> <span class="giallo-l"><span style="color: #93A1A1;"> &lt;</span><span style="color: #859900;">ComposedComponent</span><span style="color: #DC322F;"> {</span><span style="color: #859900;">...</span><span style="color: #268BD2;">this</span><span>.</span><span style="color: #268BD2;">props</span><span style="color: #DC322F;">}</span><span style="color: #93A1A1;"> /&gt;</span></span> <span class="giallo-l"><span style="color: #93A1A1;"> &lt;/</span><span style="color: #859900;">ApolloProvider</span><span style="color: #93A1A1;">&gt;</span></span> <span class="giallo-l"><span> )</span></span> <span class="giallo-l"><span> }</span></span> <span class="giallo-l"><span> }</span></span> <span class="giallo-l"><span>}</span></span></code></pre><h2 id="prefetching-data"><a class="zola-anchor" href="#prefetching-data" aria-label="Anchor link for: prefetching-data">🔗</a> Prefetching Data</h2> <p>Next has <a rel="noopener nofollow noreferrer external" target="_blank" href="https://github.com/zeit/next.js#with-link-1">in-built</a> support for prefetching pages by adding the <code>prefetch</code> prop to the <code>&lt;Link&gt;</code> component or imperatively calling <code>Router.prefetch('/dynamic')}</code>. However this fetches the page’s JS code but not its data/initial props.</p> <p>This results in instant page loads but, some would argue even worse, a loading indicator shown for however long it takes to get the relevant page data from your API.</p> <p>Since we know our stack we can optimise performance using that knowledge. We know that with every call to <code>getInitialProps</code> of a component wrapped in our <code>WithData</code> HOC, our queries are cached in the local ApolloClient store. With that all we have to do is get a reference to our top level page component and manually call its <code>getInitialProps</code> method!</p> <pre class="giallo" style="color: #657B83; background-color: #FDF6E3;" ><code data-lang="javascript"><span class="giallo-l"><span style="color: #859900;">import</span><span style="color: #268BD2;"> Router</span><span style="color: #859900;"> from</span><span style="color: #2AA198;"> &#39;next/router&#39;</span></span> <span class="giallo-l"><span style="color: #859900;">import</span><span> {</span><span style="color: #268BD2;"> format</span><span>,</span><span style="color: #268BD2;"> resolve</span><span>,</span><span style="color: #268BD2;"> parse</span><span> }</span><span style="color: #859900;"> from</span><span style="color: #2AA198;"> &#39;url&#39;</span></span> <span class="giallo-l"></span> <span class="giallo-l"><span style="color: #859900;">export default</span><span style="color: #268BD2;"> prefetch</span><span style="color: #859900;"> =</span><span style="color: #586E75;font-weight: bold;"> async</span><span> (href)</span><span style="color: #586E75;font-weight: bold;"> =&gt;</span><span> {</span></span> <span class="giallo-l"><span style="color: #93A1A1;font-style: italic;"> // if we&#39;re running server side do nothing</span></span> <span class="giallo-l"><span style="color: #859900;"> if</span><span> (</span><span style="color: #859900;">typeof</span><span style="color: #268BD2;"> window</span><span style="color: #859900;"> ===</span><span style="color: #2AA198;"> &#39;undefined&#39;</span><span>)</span><span style="color: #859900;"> return</span></span> <span class="giallo-l"></span> <span class="giallo-l"><span style="color: #586E75;font-weight: bold;"> const</span><span style="color: #268BD2;"> url</span><span style="color: #859900;"> =</span></span> <span class="giallo-l"><span style="color: #859900;"> typeof</span><span style="color: #268BD2;"> href</span><span style="color: #859900;"> !==</span><span style="color: #2AA198;"> &#39;string&#39;</span></span> <span class="giallo-l"><span style="color: #859900;"> ?</span><span style="color: #268BD2;"> format</span><span>(</span><span style="color: #268BD2;">href</span><span>)</span></span> <span class="giallo-l"><span style="color: #859900;"> :</span><span style="color: #268BD2;"> href</span></span> <span class="giallo-l"></span> <span class="giallo-l"><span style="color: #586E75;font-weight: bold;"> const</span><span> {</span><span style="color: #268BD2;"> pathname</span><span> }</span><span style="color: #859900;"> =</span><span style="color: #268BD2;"> window</span><span>.</span><span style="color: #268BD2;">location</span></span> <span class="giallo-l"></span> <span class="giallo-l"><span style="color: #586E75;font-weight: bold;"> const</span><span style="color: #268BD2;"> parsedHref</span><span style="color: #859900;"> =</span><span style="color: #268BD2;"> resolve</span><span>(</span><span style="color: #268BD2;">pathname</span><span>,</span><span style="color: #268BD2;"> url</span><span>)</span></span> <span class="giallo-l"></span> <span class="giallo-l"><span style="color: #586E75;font-weight: bold;"> const</span><span> {</span><span style="color: #268BD2;"> query</span><span> }</span><span style="color: #859900;"> =</span></span> <span class="giallo-l"><span style="color: #859900;"> typeof</span><span style="color: #268BD2;"> href</span><span style="color: #859900;"> !==</span><span style="color: #2AA198;"> &#39;string&#39;</span></span> <span class="giallo-l"><span style="color: #859900;"> ?</span><span style="color: #268BD2;"> href</span></span> <span class="giallo-l"><span style="color: #859900;"> :</span><span style="color: #268BD2;"> parse</span><span>(</span><span style="color: #268BD2;">url</span><span>,</span><span style="color: #B58900;"> true</span><span>);</span></span> <span class="giallo-l"></span> <span class="giallo-l"><span style="color: #93A1A1;font-style: italic;"> // get component reference</span></span> <span class="giallo-l"><span style="color: #586E75;font-weight: bold;"> const</span><span style="color: #268BD2;"> Component</span><span style="color: #859900;"> = await</span><span style="color: #268BD2;"> Router</span><span>.</span><span style="color: #268BD2;">prefetch</span><span>(</span><span style="color: #268BD2;">parsedHref</span><span>)</span></span> <span class="giallo-l"></span> <span class="giallo-l"><span style="color: #93A1A1;font-style: italic;"> // fetch the component props</span></span> <span class="giallo-l"><span style="color: #93A1A1;font-style: italic;"> // and cache locally, handled within getInitialProps</span></span> <span class="giallo-l"><span style="color: #859900;"> if</span><span> (</span><span style="color: #268BD2;">Component</span><span style="color: #859900;"> &amp;&amp;</span><span style="color: #268BD2;"> Component</span><span>.</span><span style="color: #268BD2;">getInitialProps</span><span>) {</span></span> <span class="giallo-l"><span style="color: #586E75;font-weight: bold;"> const</span><span style="color: #268BD2;"> ctx</span><span style="color: #859900;"> =</span><span> { pathname:</span><span style="color: #268BD2;"> href</span><span>,</span><span style="color: #268BD2;"> query</span><span>, isVirtualCall:</span><span style="color: #B58900;"> true</span><span> }</span></span> <span class="giallo-l"><span style="color: #859900;"> await</span><span style="color: #268BD2;"> Component</span><span>.</span><span style="color: #268BD2;">getInitialProps</span><span>(</span><span style="color: #268BD2;">ctx</span><span>)</span></span> <span class="giallo-l"><span> }</span></span> <span class="giallo-l"><span>}</span></span></code></pre> <p>We can then call this throughout our application, like for example: <code>&lt;a onMouseOver={() =&gt; prefetch("/product?sku=0001")}&gt;</code></p> <p>This will load not only our product.js page but also all the specific graphql queries into the ApolloClient store.</p>
Liked this article? Share this with others

Got any questions or comments? Drop me a message on Twitter @shalzzj

Sign up for my newsletter to be the first to know about a new post