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
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.
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 :)
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.
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 ✌️
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.
Loading on render was slow as heck:
Worked but just as slow as running a query without Suspense.
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
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 😛
Continue reading about Towards a Gatsby+Suspense proof-of-concept
Semantically similar articles hand-picked by GPT-4
- React 18 and the future of async data
- Async React with NextJS 13
- Prefetch data with React Query and NextJS – CodeWithSwiz 8, 9
- Experimenting with the new React Concurrent mode
- How React Query gives you almost everything you thought you needed GraphQL for
I write articles with real insight into the career and skills of a modern software engineer. "Raw and honest from the heart!" as one reader described them. Fueled by lessons learned over 20 years of building production code for side-projects, small businesses, and hyper growth startups. Both successful and not.
Subscribe below 👇
Join Swizec's Newsletter and get insightful emails 💌 on mindsets, tactics, and technical skills for your career. Real lessons from building production software. No bullshit.
"Man, love your simple writing! Yours is the only newsletter I open and only blog that I give a fuck to read & scroll till the end. And wow always take away lessons with me. Inspiring! And very relatable. 👌"
Senior Mindset Book
Get promoted, earn a bigger salary, work for top companiesLearn more
Have a burning question that you think I can answer? Hit me up on twitter and I'll do my best.
Who am I and who do I help? I'm Swizec Teller and I turn coders into engineers with "Raw and honest from the heart!" writing. No bullshit. Real insights into the career and skills of a modern software engineer.
Want to become a true senior engineer? Take ownership, have autonomy, and be a force multiplier on your team. The Senior Engineer Mindset ebook can help 👉 swizec.com/senior-mindset. These are the shifts in mindset that unlocked my career.
Curious about Serverless and the modern backend? Check out Serverless Handbook, for frontend engineers 👉 ServerlessHandbook.dev
Want to Stop copy pasting D3 examples and create data visualizations of your own? Learn how to build scalable dataviz React components your whole team can understand with React for Data Visualization
Did someone amazing share this letter with you? Wonderful! You can sign up for my weekly letters for software engineers on their path to greatness, here: swizec.com/blog
By the way, just in case no one has told you it yet today: I love and appreciate you for who you are ❤️