Messing around with React Suspense gave me a great idea: What if you could fallback to preloaded data instead of a loading spinner? 💡
You could leverage Gatsby’s approach with React Suspense for a smooth user experience. Load partial data at build time, bake it into the static HTML, then load the rest when a user renders your page.
Initial experiments look great, where I’m stuck is loading data for subsequent pages. Let me explain
You can see it in action here 👉 https://gatsby-suspense-poc.now.sh/
Code on GitHub 👉 https://github.com/Swizec/gatsby-suspense-poc
Gatsby with dynamic data, some background
Let’s start with background on using Gatsby for dynamic pages.
Gatsby is a static site builder. You build a website in React, use GraphQL to connect to data sources, and compile a static HTML page.
Your GraphQL queries run at compile time and bake data into your static HTML via hardcoded props. Not quite hardcoded props but close enough.
The result is a page that can work offline and loads without API requests. This makes it fast.
When your site loads in a browser, however, it becomes a React single page app. React hydration means there’s no flash of blank content and no need to re-render the HTML.
You can add Apollo Client for GraphQL support or use
I like to wrap the root node in ApolloProvider and use react-apollo-hooks for the rest.
That gives my entire app access to GraphQL for a specific server. Using Star Wars API for this example.
Loading additional data on render
Once you have “cached” data baked into your HTML and the ability to load fresh data on render, you’re presented with a dilemma.
How do I show static data, communicate that more data is loading, and show fresh data when I’ve got it?
If the user is offline, what then?
Those are tough problems. Traditionally, you’d render a component, show a spinner, fetch more data after render, then re-render once done.
Something like this (this is pseudocode):
STARSHIP_QUERY is a GraphQL query that returns a list of
$count starships from StarWarsApi,
swapi for short.
useQuery runs the query and returns a loading state and data.
Component renders the loading notification and a list of starships from props. Those were baked in at build time. When fresh data arrives from our API call, the component re-renders.
You can tell this is pseudocode because I’m re-defining the
data constant and that won’t work 🙂
Using React Suspense instead
With React Suspense, you can make that API call preemptively. While the page is loading.
Some background on how that works in last week’s Experimenting with the new React Concurrent mode article ✌️
Here’s how you might set it up for Gatsby. Or at least how I did.
All this goes in the
index.js page. The first page you see in a Gatsby app.
We start with a static page query for Gatsby. This one runs at build-time and bakes data into our HTML. I tried having a shared query between Gatsby and Apollo, but the
graphql (gatsby) and
gql (apollo) tags are not compatible.
Then we’ve got the
<Starships> component suspends with a data load and renders
<StarshipsList> when data shows up. The static page is going to render this same component with its data.
Page render with suspendering
IndexPage component we can then use
<Suspense> to coordinate loading and error states.
Our Gatsby page gets static data via the
data prop. This works offline too, you can try, here 👌
Here’s where the fun starts:
Suspense sees that we’re running a GraphQL query (the suspender.read()) so it renders the
fallback render prop. But that prop uses our static data to render a list of 5 starships. It’s not all of them, but it’s better than nothing.
Even if it all goes to shit, our user gets the most important data they’re looking for.
Once our query finishes, Suspense renders the main list. Now with fresh data from the API.
<ErrorBoundary> gives us an extra feature: We can show something useful even if the user is offline and our query fails.
Same fallback to static data, now with a warning that you’re offline ✌️
Where I’m stuck: subsequent pages
Now that approach works great, if your site has a single page. Where it breaks down are multi-page sites. Which is most of them.
I tried replicating the same code on
page-2.js and this happens:
swapi requests on initial page load. Makes subsequent pages crazy fast with no hint of data loading, but means that you’re loading data for your whole website on initial load.
That just won’t do. We need to hook into the router’s lifecycle. Gatsby uses @reach/router by the way.
And that’s where I’m stuck. Tried a few things, none worked.
Suspend on render, nope
Loading on render was slow as heck:
Worked but just as slow as running a query without Suspense.
Pass suspender as link state, nah
Reach Router lets you pass state to pages with a
state prop in your
<Link> component. Whatever you pass shows up in the
location prop on the target component.
Works great for static values, but did not work with this approach:
Not sure why, but trying to pass a function made the whole state value
Hook into onPreRouteUpdate, nyet
Another approach I tried was using Gatsby’s browser APIs. You can hook into various parts of the page lifecycle with special methods in
Thought I could manipulate the
location prop passed into a page and I can, sort of.
When you console.log that value, it shows that you successfully passed the
dataSuspender. This was great news until I realized that data shows up after the page renders.
How do I know?
console.log(location) shows the value, but
console.log(location.dataSuspender) prints undefined. Means the value isn’t there when you’re printing, but does show up later. Hooray for console.log being dynamic and always showing current object state.
I tried everything I could think of. Some of this stuff is either a bug in Gatsby, a bug in @reach/router, or just not meant to work this way by design.
Next step: bug people online to find out 😛
Learned something new? Want to improve your skills?
Join over 10,000 engineers just like you already improving their skills!
Here's how it works 👇
PS: You should also follow me on twitter 👉 here.
It's where I go to shoot the shit about programming.