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

    Async React with NextJS 13

    Wanna see something cool? React is getting native async support and you can already try it out ๐Ÿ˜

    This will soon work anywhere:

    const ShowData = async () => {
      const res = await fetch("/data-source")
      const json = res.json()
    
      return <p>{json.text}</p>
    }
    

    Note the async React component, the await in its body, the complete lack of any loading states, effects, hooks, or libraries. It just works. You can use this component anywhere in your tree โ€“ even in a component that isn't itself async!

    This is part of React's RFC: First class support for promises and async/await. A next step from React Suspense, which I wrote about in React 18 and the future of async data.

    The goal is to make React Suspense easier to use. It worked!

    Parallel loading states in a NextJS 13 app

    You can use async React in NextJS 13

    Right now the best way to try React's experimental support for promises is with the beta side of NextJS 13 โ€“ the /app directory. I used it to build ScholarStream.ai and it feels weird, but works great.

    You can see my full code on GitHub.

    The /app directory embraces Server Components โ€“ React components that render on the server and send plain HTML over the wire. No client-side JavaScript! Every re-render goes back to the server.

    Now I hear you thinking "But Swiz, that's slow as shit!? Didn't we invent single-page-apps because roundtrips to the server take too long??"

    Yes, we did. Servers have come a long way since then.

    My understanding is that Vercel, the company behind NextJS, uses extensive Serverless and Edge function shenanigans to run the tiniest possible server as close as possible to the user to render each component. Like Remix, they need a custom compiler built into NextJS to make this work smoothly.

    Hybrid renders with NextJS, picture from docs
    Hybrid renders with NextJS, picture from docs

    When a server component needs to render, a new function spins up for just that render. With the right config (and payment tier), that function runs on a CDN-like platform that aims for low latency. The function then returns just that component's HTML and NextJS replaces the right section of your UI in the browser.

    I don't know how possible/easy it is to do this without Vercel. In theory NextJS is a standalone framework that toooootally works fine without Vercel.

    Yes that means that even with server components there's plenty of client-side JavaScript left. But there's less :)

    What async React looks like with beta NextJS

    Again, full code on GitHub ๐Ÿ‘‰ https://github.com/Swizec/ScholarStream.ai. See it in action ๐Ÿ‘‰ ScholarStream.ai

    The base case is to use NextJS 13's new opinionated /app structure:

    • page.tsx for the page
    • layout.tsx for the static layout
    • loading.tsx for the loading state
    Loading state gif

    page.tsx

    Page components are always server components. NextJS renders on the server, caches the result, and returns.

    // app/[route]/page.tsx
    export default async function Home() {
      return (
        <main className={styles.main}>
          <Pitch />
    
          <h2>Read about:</h2>
          <TopicsList />
    
          {/* @ts-expect-error Server Component */}
          <Feed topic="cs.AI" count={5} isLast />
        </main>
      )
    }
    

    You need to tell TypeScript to expect an error when using async (server) components inside a JSX tree. It works, but types don't know about it yet. The NextJS team is working on getting that updated upstream.

    <Feed> in this case is the component that performs async data loading. NextJS seamlessly handles that no fuss.

    layout.tsx

    Layout components tell NextJS what to always render around your page. When you nest subdirectories to make complex routes, their layouts also nest.

    // app/[route]/layout.tsx
    export default function RootLayout({
      // Layouts must accept a children prop.
      // This will be populated with nested layouts or pages
      children,
    }: {
      children: React.ReactNode
    }) {
      return (
        <html lang="en">
          <Script
            src="https://plausible.io/js/script.js"
            data-domain="scholarstream.ai"
          />
          <body>
            <nav className={styles.topNav}>
              <Link href="/about">About</Link>
            </nav>
            {children}
            <div className={styles.footer}>
              built with reckless abandon by <a href="https://swizec.com">Swizec</a>
              <br />
              Thank you to arXiv for use of its open access interoperability.
            </div>
          </body>
        </html>
      )
    }
    

    loading.tsx

    The loading component renders while waiting for the page component's promise to resolve.

    // app/[route]/loading.tsx
    export default function PageLoading() {
      return (
        <main className={styles.main}>
          <FeedLoader />
        </main>
      )
    }
    

    An interesting challenge here is that I couldn't find any open source React spinner components that worked. The animation wouldn't fire ๐Ÿคจ

    Loading data

    Loading data is the typical example of a slow operation that requires promises. But you can use the same techniques for anything.

    Like with React Query, the recommendation is to load data close to where it's used. Same component is best. You can think of it as declaring a data dependency in your component and letting React and NextJS handle the details.

    For example, here's how I load an arXiv feed:

    // loads list of articles
    // renders in a loop
    export const FeedInnards = async (props: FeedProps) => {
      const { offset = 0, count = 10 } = props
    
      let feed: arxiv.ArxivFeed
      let papers: arxiv.ArxivFeedItem[]
    
      try {
        feed = await arxiv.getFeed(props.topic)
        papers = feed.items.slice(offset, count)
      } catch (e) {
        console.error(e)
    
        return (
          <>
            <p>Error loading feed. Try one of these topics instead:</p>
            <TopicsList />
          </>
        )
      }
    
      return (
        <>
          {papers.map((paper) => (
            // @ts-expect-error Server Component
            <FeedItem paper={paper} key={paper.link} />
          ))}
        </>
      )
    }
    

    Notice the await in the body of that component, that's the shiny new toy! React/NextJS shows a loading state while this component's promise is pending. You don't have to deal with that ๐Ÿ˜

    You do have to write your own try/catch logic, however, because error boundaries don't work with async components. Yet?

    Extended fetch with caching

    The data fetching behind my <FeedInnards> component looks like this:

    export async function getFeed(category: string): Promise<ArxivFeed> {
      const parser: Parser<Omit<ArxivFeed, "items">, ArxivFeedItem> = new Parser()
      const feed = await fetch(
        `http://export.arxiv.org/rss/${category}?version=1.0`,
        {
          next: { revalidate: TEN_HOURS },
        }
      ).then((r) => r.text())
    
      try {
        const parsed = await parser.parseString(feed)
        return parsed
      } catch (e) {
        throw new Error("Could not parse feed")
      }
    }
    

    Fetch RSS feed from arXiv using a standard fetch() then parse with an RSS parser. Nothing crazy.

    But notice the extra params in that fetch call:

    await fetch(`http://export.arxiv.org/rss/${category}?version=1.0`, {
      next: { revalidate: TEN_HOURS },
    })
    

    NextJS adds a custom param to fetch() that lets you specify a caching behavior. You can enable/disable the cache and specify revalidation behavior.

    In my case, the app fetches a fresh feed every 10 hours. Reload the page before then and you'll get a stable result with no loading indicators. Cache is stable across users and most visitors get a fast nearly static page.

    Fast page on reload

    It's unclear to me where that cache lives. Is this something that works with NextJS or just with Vercel? ๐Ÿค”

    Cache without fetch for 3rd party libs

    When you don't control the underlying API call (like with a library), you can cache results using the new React.cache() method. Useful for any slow operation because it works on functions that return promises rather than hooking into fetch() itself.

    For example when I'm using OpenAI to create summaries:

    const getSummary = cache(async (paper: arxiv.ArxivFeedItem) => {
      const summary = await openai.getSummary(paper)
      return summary
    })
    
    const PaperSummary = async (props: { paper: arxiv.ArxivFeedItem }) => {
      const summary = await getSummary(props.paper)
    
      return <p>{summary.choices[0].text}</p>
    }
    

    Going hard on the idea that "You should load data close to where it's used", I have a component that gets a paper, calls OpenAI to summarize, and renders a single paragraph.

    The OpenAI call is wrapped in cache() to increase performance. For cost optimization I use additionally Redis caching on top. More on that another time :)

    Custom Suspense boundaries

    Page level loading states are great, but you may want more fine-grained control. Or to load components in parallel with optimistic first-come rendering.

    Parallel loading states with suspense boundaries

    You do that with <Suspense> boundaries.

    For example, when you know the summary portion of a <FeedItem> is slower than the rest:

    const FeedItem = async (props: { paper: arxiv.ArxivFeedItem }) => {
      const { paper } = props
    
      // ...
    
      return (
        <div className={feedStyles.item}>
          // ...
          <Suspense fallback={<RingLoader color="blue" loading />}>
            {/* @ts-expect-error Server Component */}
            <PaperSummary paper={paper} />
          </Suspense>
          <div>
            Full paper at ๐Ÿ‘‰{" "}
            <a href={paper.link} className={feedStyles.linkPaper}>
              {paper.title.split(/(\(|\. \()arXiv/)[0]}
            </a>
          </div>
        </div>
      )
    }
    

    Using the <Suspense> boundary is like telling React to "Render the <FeedItem> component, but show a fallback state while promises inside this part of the component tree are pending".

    The suspense boundary will capture every promise in its children. You don't have to coordinate anything.

    You can pepper suspense boundaries wherever it makes sense for your app. Even the page level loading state is a suspense boundary under the hood.

    Final thoughts

    I love it! Async React is going to simplify a lot of my code.

    But I'm worried that this will be hard to implement outside NextJS and Vercel. We'll see.

    Cheers,
    ~Swizec

    PS: the beta docs for NextJS 13 are fantastic

    Did you enjoy this article?

    Published on January 14th, 2023 in ScholarStream, React, NextJS, Frontend, Technical, Serverless

    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

    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 โค๏ธ