Swizec Teller - a geek with a hatswizec.com

Senior Mindset Book

Get promoted, earn a bigger salary, work for top companies

Senior Engineer Mindset cover
Learn more

    Prefetch data with React Query and NextJS โ€“ CodeWithSwiz 8, 9

    Prefetching lets you start React apps without loading spinners. Gatsby introduced this concept with staticQuery, react-query and NextJS make the pattern easier ๐Ÿ˜

    CodeWithSwiz is a twice-a-week live show. Like a podcast with video and fun hacking. Focused on experiments. Join live Wednesdays and Sundays

    It took two episodes to figure this out and verify it works. Here they are ๐Ÿ‘‡

    https://www.youtube.com/watch?v=_t66WKiX6Z4

    We're building towards a headless CMS. The component that needs prefetching is a social card.

    Prefetchable component

    export const SocialCardImage = ({ title }) => {
    const cardQuery = useSocialCardQuery(title)
    if (cardQuery.isLoading) {
    return <Spinner />
    }
    return <Image src={cardQuery.data.url} />
    }

    The SocialCardImage component uses a query to create a social card for a title. When the query isLoading, we show a spinner, when it's done, we show the image.

    Component with spinner

    We built the useSocialQuery custom hook in a past CodeWithSwiz episode. A thin wrapper on react-query's useQuery makes it more reusable.

    export function useSocialCardQuery(title) {
    return useQuery(["social-card", title], fetchSocialCard, {
    staleTime: 5000,
    })
    }

    Name the query "social-card", add title as a parameter, use the fetchSocialCard data loader, and set staleTime to 5 seconds. Helps with query reuse and avoids refetching.

    The data loader talks to my screenshot AWS Lambda to create social cards.

    async function fetchSocialCard(key, title) {
    if (title) {
    const res = await fetch(
    `https://pifc233qp6.execute-api.us-east-1.amazonaws.com/dev/social-card?title=${title}`
    )
    return res.json()
    } else {
    return null
    }
    }

    Basic fetch() request to a lambda. The lambda spins up a Chrome browser, loads a special page on my blog, renders the card, takes a screenshot, saves it to S3, returns a URL.

    How screenshots work
    How screenshots work

    What is prefetching

    The SocialCardImage component works great. Slow, but great.

    Pop it on a page, add input from a form, users get a social card. Everyone expects a spinner. Can't load something before the user says what.

    What if you load this as a new page?

    Fill out the form, click submit, get redirected. Would you expect loading spinners like this?

    Spinners after load

    Or would you think "The site has data, why am I waiting twice?"

    Granted, it's 2020 and you wouldn't expect the site to reload after a form submit. But I wanted to figure out prefetching ๐Ÿ˜‡

    Prefetching data with NextJS

    NextJS offers two convenient ways to pre-define data for your page:

    • getStaticProps for static data
    • getServerSideProps for dynamic server data

    One is meant for SSR โ€“ย server side rendering โ€“ย and the other for SSH โ€“ย server side generation. Which is which feels unclear from the docs.

    Both enable you to bake data into the initial HTML for your page. The difference is when this baking happens.

    Both work the same way:

    1. NextJS starts building a page
    2. Calls your exported getStaticProps or getServerSideProps function
    3. Function does stuff
    4. Returns a props object
    5. NextJS renders your page with those props

    getServerSideProps with NextJS dynamic routing

    To render the SocialCardImage with a pre-defined article title based on a URL slug, you'd write a page like this:

    // pages/[slug].js
    export async function getServerSideProps(context) {
    const { slug } = context.params
    const article = magicallyReadArticle(slug)
    return {
    props: {
    article
    }
    }
    }
    export default ArticlePreview({ article }) {
    return <SocialCardImage title={article.title} />
    }

    Naming your file [slug] gives you nextjs dynamic routing โ€“ย file runs for any /this-is-slug URL and renders the ArticlePreview page component.

    NextJS runs getServerSideProps with the routing context and you use the slug to find your article in a storage system. We used temp files but that doesn't work in production.

    The function returns your article as a prop, it gets passed into the page component, and the user gets a preview.

    And they get a spinner.

    Spinners after load

    Add React Query to the mix

    You can fix the spinner by manually fetching the social card in getServerSideProps. And that means duplicating your logic, dealing with checking 2 data sources, etc.

    You've built the query before. You've got a component that works. Why rebuild it all?? ๐Ÿคจ

    Here's what you do instead:

    1. Wrap your app in React Query cache
    2. Prefetch query in getServerSideProps
    3. Dehydrate the full cache on page load
    4. Keep frontend code the same
    // pages/_app.js
    const queryCache = new QueryCache()
    function MyApp({ Component, pageProps }) {
    return (
    <ReactQueryCacheProvider queryCache={queryCache}>
    <Hydrate state={pageProps.dehydratedState}>
    <ThemeProvider theme={theme}>
    <Component {...pageProps} />
    </ThemeProvider>
    </Hydrate>
    </ReactQueryCacheProvider>
    )
    }

    Create a new query cache, wrap the app in <ReactQueryCacheProvider> and <Hydrate>. They're react-query's context providers for internal state sharing.

    Then prefetch your queries:

    async function prefetchQueries(article) {
    const queryCache = new QueryCache()
    await queryCache.prefetchQuery(
    ["social-card", article.title],
    fetchSocialCard
    )
    return dehydrate(queryCache)
    }
    export async function getServerSideProps(context) {
    const { slug } = context.params
    const article = magicallyReadArticle(slug)
    return {
    props: {
    dehydratedState: await prefetchQueries(article),
    article,
    },
    }
    }

    We can't reuse useSocialCardQuery because we're doing something different โ€“ย prefetching.

    Replace useQuery with queryCache.prefetchQuery and pass that into the dehydrate() call from react-query. Put the full result in the dehydratedState prop.

    The <Hydrate> component reads that from its props

    <Hydrate state={pageProps.dehydratedState}>

    And you get a page load with no spinners

    Page load with social card without spinners

    Okay the UX around that form submit needs work but you get the idea. No spinners! ๐ŸŽ‰

    Why I like this

    Way easier to use than Gatsby's useStaticQuery. Same code works with both static data and dynamic updates later.

    Data baking in Gatsby is more suited to pages that don't turn into single-page-apps. This works for both ๐Ÿ˜

    Cheers,
    ~Swizec

    Did you enjoy this article?

    Published on September 30th, 2020 in CodeWithSwiz, Technical, NextJS, Livecoding,

    Senior Mindset Book

    Get promoted, earn a bigger salary, work for top companies

    Learn 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

    Want to get my best emails on JavaScript, React, Serverless, Fullstack Web, or Indie Hacking? Check out swizec.com/collections

    Want to brush up on modern JavaScript syntax? Check out my interactive cheatsheet: es6cheatsheet.com

    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

    Want to brush up on your modern JavaScript syntax? Check out my interactive cheatsheet: es6cheatsheet.com

    By the way, just in case no one has told you it yet today: I love and appreciate you for who you are โค๏ธ

    Created by Swizec with โค๏ธ