The app directory, Streaming, Suspense, and hybrid Server and Client Components were demystified — with a little help from GraphQL + WunderGraph. is a total game-changer. Next.js 13 It has made building the next generation of the web much, much more intuitive for developers, implementing bleeding-edge React 18 features via the new app directory — like , native support (finally!), and Streaming HTML — with streamlined nested routes/layouts via folder-based routing, and better DX (and infinitely better type-safety!) in doing SSR/SSG/ISR with its extended fetch API. Server Components async/await Coving every shiny, bleeding-edge new feature in Next.js 13 would take more time than we have, so today, let’s quickly talk about the most critical part of literally any app you’ll ever develop — — building this Record Store catalog browser app on top of a Postgres database, as a learning exercise. Oh, and we’ll be using GraphQL (via ) to get data out of our database because we don’t compromise on developer experience ’round these parts. the data fetching story WunderGraph What’s Wrong With Vanilla SSR? To understand the problem in data fetching using SSR in Next.js 12 and below, let’s first talk about the sequence of events that needs to happen to get data from the server to the client. First, on receiving a request from the client for a specific page, the server fetches the data required (from the database, API, wherever). The server then renders the HTML for the page. The rendered HTML and JavaScript bundle for the page are sent to the client. Finally, React hydrates the page to make it interactive. The problem is that these steps are sequential and blocking. The server , and the users’ browsers can’t render the HTML for the page before all the data has been fetched can’t hydrate the page with JavaScript until the code for every component on the page has been downloaded. So there’s always going to be a noticeable delay between when the client requests the page and when it arrives, fully rendered and interactive. This is the (in)famous data waterfall problem. Now, you could mitigate some of this issue with code splitting (dynamic imports) or prefetching data for specific routes (with the component’s prefetch prop)...but there’s a more direct solution. <Link> What if you could pause/resume/abandon an in-process render while still ensuring a consistent, performant UI? What if a rendering pass were interruptible? That’s exactly what Next.js 13 (building on React 18’s beta features — namely, Concurrent Rendering — now lets you do. You couldn’t parallelize the render pipeline before, but now, you can progressively send chunks of HTML from the server to the client instead — — instantly rendering parts of a page that do not require data, then gradually streaming in sections that do require data as their data dependencies resolve. This enables parts of the page to be displayed sooner, without waiting for all the data to load before any UI can be rendered. the server keeps a connection open, sending a stream of UI, as it is rendered, to the frontend Reducing Time To First Byte (TTFB) and First Contentful Paint (FCP) this way ensures a better user experience, especially on slower connections and low-powered devices. Streaming SSR — The How-To Next.js 13 gives you two ways to implement Streaming SSR. Let’s take a look at both, in code. I’ll be using a Postgres data source (the famous database) for my example, using WunderGraph to make it accessible through JSON-RPC to my Next.js front end. Chinook Now, for querying this database, you can use whichever method you prefer — I’m using WunderGraph. It’s a crazy huge DX win when used as an API Gateway or Backend-for-Frontend — introspecting your data sources (no matter if they’re Apollo federations, OpenAPI-REST APIs, GraphQL APIs, relational or document-based databases; a full list) and consolidating them into a virtual graph layer that you can define type-safe operations on, and access via JSON-RPC — using GraphQL only as a development tool, not a public endpoint. here’s Let’s get started. Step 0A: Setting Up the Database First off, the easiest way to get a Postgres database going is with , and that’s what I’m doing here. But since these databases use TCP connection strings, you could use literally any Postgres host you want — including DBaaS ones like Railway. Docker docker run --name mypg -e POSTGRES_USER=myusername -e POSTGRES_PASSWORD=mypassword -p 5432:5432 -d postgres This will set up a Docker Container named for you, with the username and password you specify, at port 5432 (localhost), using the official (it’ll download that for you if you don’t have it locally) mypg postgres Docker Image I’m assuming you’ll have your own data to go along with this, but if you don’t, get the file from the , and run it (perhaps via a Postgres client like ) to seed the Chinook database (it’s a record store’s catalog; artists, their albums, its songs, with some sample transactions and invoices) Chinook_PostgreSql.sql official Chinook repo here pgAdmin Step 0B: Setting Up WunderGraph + Next.js Secondly, we can set up both the WunderGraph server and Next.js using WunderGraph’s , so let’s do just that. create-wundergraph-app CLI npx create-wundergraph-app my-project -E nextjs When that’s done, cd into the directory you just created (you didn’t leave as is, did you…?), and . Head on over to and you should see the WunderGraph + Next.js starter splash page pop up with the results of a sample query, meaning everything went well. my-project npm i && npm start localhost:3000 Step 0C: (Optionally) Setting Up Tailwind CSS for Styling I love utility-first CSS, so I’m using Tailwind for styling throughout this tutorial. Install instructions . here You could use any other styling approach you want, but remember styling solutions — including component libraries — that depend on CSS-in-JS engines like **emotion** do not currently work with the new app directory structure. Step 1: Defining Our Data Check out in the directory in your root. wundergraph.config.ts .wundergraph const spaceX = introspect.graphql({ apiNamespace: 'spacex', url: 'https://spacex-api.fly.dev/graphql/', }); // configureWunderGraph emits the configuration configureWunderGraphApplication({ apis: [spaceX], //... }) See this? That’s how easy it is to add your data sources as dependencies when using WunderGraph. Define your data sources as JavaScript/TypeScript variables, tell WunderGraph to introspect them, and then add them to your project config as a dependency array. , making onboarding, maintenance, and iteration much, much easier for you as a developer. This Configuration-as-code approach means your front end can stay entirely decoupled Following this pattern, let’s add our Postgres database. const db = introspect.postgresql({ apiNamespace: "db", databaseURL: `postgresql://${process.env.DB_PG_USER}:${process.env.DB_PG_PWD}@${process.env.DB_PG_HOST}:5432/chinook`, }); // configureWunderGraph emits the configuration configureWunderGraphApplication({ apis: [db], //... }) I’m using a Postgres instance at , but you might not be, so using ENV variables for my DB connection string keeps this tutorial dynamic. Add your username, password, and host values to accordingly — localhost .env.local TL;DR : Add your PostgreSQL connection string as the **databaseURL** here. Save this config file, and WunderGraph will consolidate this data into a virtual graph. Now you’ll need to define the operations you want to actually get data . WunderGraph makes getting the exact relations you want in one go (as well as cross-source data JOINs!) a cakewalk with GraphQL. out of it Create an file in . AlbumById.graphql .wundergraph/operations query ($albumId: Int!) { Album: db_findUniqueAlbum(where: { AlbumId: $albumId }) { Title Artist { Name } Track { TrackId Name Composer Milliseconds } } } Now, when you hit save in your IDE, the WunderGraph server will build the types required for the queries and mutations you’ve defined on all of your data sources, and generate a custom Next.js client with typesafe hooks you can use in your front-end for those operations. How cool is that?! Step 2: Building It in Next.js 12 First . You can still have parts of your app in the old pages directory, but everything you put in the new app directory will opt into the beta features — you’ll just have to make sure none of your routes conflict between the two. Next.js 13 is built for progressive adoption This affords us a unique opportunity for this tutorial — build our app the old-fashioned way — using Next.js 12 (and the pages directory)…and then upgrade it to Next.js 13 (using the app directory) taking full advantage of nested layouts, hybrid architecture (Server Components and Client Components together) and Streaming + Suspense. Fun! So let’s get the bulk of the work out of the way, first. 1. _app.tsx import Head from 'next/head'; import Navbar from '../components/Navbar'; function MyApp({ Component, pageProps }) { return ( <> <Head> <meta charSet="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <script src="https://cdn.tailwindcss.com"></script> </Head> <main className="text-cyan-500 bg-zinc-900 min-h-screen justify-center"> <Navbar /> <Component {...pageProps} /> </main> </> ); } export default MyApp; 2. index.tsx import { NextPage } from "next"; import { useState } from "react"; import DataTable from "../components/DataTable"; import { useQuery, withWunderGraph } from "../components/generated/nextjs"; const Home: NextPage = () => { const [albumId, setAlbumId] = useState(7); const [newValue, setNewValue] = useState(null); const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => { setNewValue(parseInt(event.target.value)); }; const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault(); setAlbumId(newValue); }; // data.tracks is of type AlbumByIdResponseData, automatically typed. const { data } = useQuery({ operationName: "AlbumById", input: { albumId: albumId, //7 by default }, enabled: true, }); return ( <div> <div className="relative w-full px-4 pt-4"> <div> <div className="absolute right-0 top-0 flex items-center justify-center mt-2 mx-8 text-sm text-white"> 🍵Using Next.js 12! </div> <div className="flex items-center justify-center w-full p-4 mt-4 mb-4"> <form onSubmit={handleSubmit} className="flex "> <label className="py-2 px-4 text-black bg-sky-700 text-lg font-bold rounded-l-xl"> AlbumID </label> <input type="number" value={newValue} onChange={handleChange} className="form-input w-32 py-2 px-4 text-black text-lg font-bold" /> <button type="submit" className="bg-sky-700 hover:bg-cyan-400 text-white text-lg font-bold py-2 px-4 ml-0 hover:border-blue-500 rounded-r-xl" > Search </button> </form> </div> {!newValue ? ( <div className="flex items-center justify-center mx-5 text-lg text-white"> Enter an Album ID! </div> ) : ( <></> )} {data?.Album && <DataTable data={data} />} </div> </div> </div> ); }; export default withWunderGraph(Home); is the WunderGraph-generated typesafe data fetching hook we’ll be using to get our data. useQuery Essentially, you call with an options object, specifying – useQuery an operation by name (the filename of the GraphQL operation you created in the previous step), pass in an as an input (and our form UI lets you increment that instead of typing it in manually), AlbumID And in , get back its output – a tracklist for the specified album, with composer and runtime (in milliseconds; you can use a utility function to convert that to a more readable string format) data Hh:Mm:Ss 3. DataTable.tsx import secondsToTime from "../utils/secondsToTime"; import { AlbumByIdResponseData } from "./generated/models"; // No need to define your own types/interfaces for passed props. Just import and use the autogenerated one. Nifty! type Props = { data: AlbumByIdResponseData; }; const DataTable = ({ data }: Props) => ( <div className="flex flex-col items-center "> <p className="text-3xl text-white font-bold mt-8"> &quot;{data?.Album?.Title}&quot; </p> <p className="text-lg text-cyan-100 font-bold mt-2 mb-12"> {data?.Album?.Artist?.Name} </p> <table className="table-fixed w-full"> <thead> <tr> <th className="border-2 px-4 py-2">Name</th> <th className="border-2 px-4 py-2">Composer</th> <th className="border-2 px-4 py-2">Length</th> </tr> </thead> <tbody> {data?.Album?.Track.map((track) => ( <tr className="hover:bg-cyan-500 hover:text-zinc-900 hover:font-bold cursor-pointer" key={track.TrackId} > <td className="border-2 px-4 py-2 ">{track.Name}</td> <td className="border-2 px-4 py-2 ">{track.Composer}</td> <td className="border-2 px-4 py-2 "> {secondsToTime(track.Milliseconds / 1000)} </td> </tr> ))} </tbody> </table> </div> ); export default DataTable; Not much to see here, this just displays the data in a fancy table. For completion’s sake, here’s the utility function this uses. secondsToTime export default function secondsToTime(e: number) { const h = Math.floor(e / 3600) .toString() .padStart(2, "0"), m = Math.floor((e % 3600) / 60) .toString() .padStart(2, "0"), s = Math.floor(e % 60) .toString() .padStart(2, "0"); return `${h}:${m}:${s}`; } Got all of that? Whew. Take a breather. Now, it’s time to get this working for Next.js 13. Step 3: Upgrading to Next.js 13 Part 1: Configuration First off, make sure you have Node.js v16.8 or greater. Then, upgrade to Next.js v13 (and React 18). Don’t forget ESLint if you use it! npm install next@latest react@latest react-dom@latest # If using ESLint… npm install -D eslint-config-next@latest Finally, opt into Next.js 13’s app directory paradigm by explicitly stating so in next.config.js /** @type {import('next').NextConfig} */ const nextConfig = { experimental: { appDir: true, }, }; module.exports = nextConfig; Part 2: Layouts — Your New Best Friend. In Next.js 13, routing follows a folder-based hierarchy. You use folders to define routes, and special files with reserved names — , , and — to define UI, with being the minimum required. layout.js/tsx page.js/tsx loading.js/tsx page.js/tsx How does this work? For starters, your contents (including global styles) are going to move to the root which exports a default functional component, say, , that serves as the layout for the entire route segment. If is at a root level, pages in your app will inherit it. _app.tsx layout.tsx RootLayout() layout.tsx all Every other route segment nested within can have its own and (meaning you now have much more flexibility in building complex, nested layouts, without needing wrappers or a rigid global layout), and they’ll still inherit the layout of their parent route segment automatically. layout.tsx page.tsx Each exported layout component intelligently avoids unnecessary re-renders while navigating between sibling route segments. Additionally, you can colocate everything else this route segment needs (unit tests, MDX docs, storybook files) to its directory. No more messy imports. The best part? — meaning you can fetch data at the layout level for each route segment and pass them down to be rendered. You can’t use hooks and browser APIs in these Server Components, but you don’t need to — — and explicitly have snippets of your UI opt into Client Components wherever you actually need interactivity/hooks. Anything in the app directory is a React Server Component by default you can now natively async/await data directly in Server Components without needing the very type-unsafe **getServerSideProps** pattern So what does that mean for the app we’re building? It means our directory is going to end up looking like this. app Let’s tackle this one by one, root level up. 1. layout.tsx import Navbar from "../components/Navbar"; /* The root layout replaces the pages/_app.tsx and pages/_document.tsx files. */ 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"> <body> <header> <Navbar /> </header> <main>{children}</main> </body> </html> ); } 2. head.tsx export default function Head() { return ( <> <title>My Next.js App</title> <meta charSet="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> </> ); } You could have any external scripts you require placed here, too (as a imported from ). <Script> next/script 3. app/albums/layout.tsx import React from "react"; import AlbumForm from "../../components/AlbumForm"; type Props = { children: React.ReactNode; }; export default function AlbumsLayout(props: Props) { return ( <div className="relative text-cyan-500 bg-zinc-900 min-h-screen"> <div className="absolute right-0 top-0 mt-2 mx-8 text-sm text-white"> ☕Using Next.js 13! </div> <div className="w-full px-4 pt-4"> <div> {/* client component */} <div className="flex items-center justify-center w-full p-4 mt-4 mb-4"> <AlbumForm /> </div> {/* server component DataTable goes here */} {props.children} </div> </div> </div> ); } An important gotcha to keep in mind: If this were a Client Component, They’d have to be nested children, not direct imports; else they’d degenerate into Client Components too and you’d get an error. you couldn’t use a Server Component in it directly. 4. app/albums/page.tsx export default function AlbumsPage() { return ( <div> <div className="flex items-center justify-center mx-5 text-lg text-white"> Enter an Album ID! </div> </div> ); } Don’t be alarmed if your route segment ’s end up being minimal! page.tsx Part 3: Streaming SSR For the actual Streaming HTML implementation, Next.js 13 gives you two choices. Option A: Instant Loading States with loading.tsx Use a special file called , that lets you specify a canonical ‘Loading’ state for all your data fetching at a route segment level. loading.js/tsx The idea is simple: If one of your components within a route segment isn’t ready to render yet (no data, bad data, slow connection; whatever) Next.js is going to show the UI rendered by this file in place of your data — and then . All navigation is immediate and interruptible. . automatically replace it with your actual component’s UI + data once the latter’s rendering is complete You aren’t blocked waiting for the route contents to load fully before navigating to another route Using a this way will . You can pre-render critical UI chunks (loading indicators such as skeletons and spinners, or a product image, name, etc) while non-critical chunks of the UI (comments, reviews, etc.) stream in. loading.tsx let you designate critical and non-critical parts of a page This is good UX design because it lets users understand that something — — is happening, and that your app didn’t freeze and/or crash. anything 4. app/albums/[id]/loading.tsx export default function Loading(): JSX.Element { return ( <div className="flex items-center justify-center h-screen"> <p className="text-white font-bold text-2xl"> Loading (with loading.tsx!)... </p> </div> ); } And your is as simple as it gets. No need to manually do conditional rendering, checking if data is undefined, and displaying loading messages/spinners/skeletons if it is. [id]/page.tsx If you want to use the typesafe data fetching hooks (that use under the hood — , , etc.) generated by WunderGraph, you’ll need to explicitly make this a Client Component – with a top-level module pragma-like statement. Vercel’s SWR useQuery useMutation “use client” 5. app/albums/[id]/page.tsx "use client"; import DataTable from "../../../components/DataTable"; import { useQuery } from "../../../components/generated/nextjs"; type Props = { params: Params; }; type Params = { id: string; }; export default function AlbumPage(props: Props) { /* For Client components - use the hook! */ const { data } = useQuery({ operationName: "AlbumById", input: { albumId: parseInt(props.params.id), }, suspense: true, }); return ( <div> <DataTable data={data} /> </div> ); } A common gotcha: Don’t forget to add to ’s options! suspense: true useQuery Want to reap the advantages of a React Server Component (much faster data fetching, simple native async/await, zero JS shipped to the client) instead? No problem! Using default WunderGraph configs, each operation (.graphql file) you have, is exposed as JSON-RPC (HTTP) at: [http://localhost:9991/app/main/operations/[operation_name]](http://localhost:9991/app/main/operations/[operation_name]) So now, your is going to look like this: [id]/page.tsx import { Suspense } from "react"; import { AlbumByIdResponseData } from "../../../components/generated/models"; import DataTable from "../../../components/DataTable"; type Props = { params: Params; }; type Params = { id: string; }; export default async function AlbumPage(props: Props) { let data; let res; try { res = await fetch( `http://127.0.0.1:9991/app/main/operations/AlbumById?albumId=${props.params.id}` ); // If NodeJs fetch errors out using 'localhost', use '127.0.0.1' instead if (!res.ok) throw new Error(res.statusText); let json = await res.json(); data = json.data as AlbumByIdResponseData; } catch (err) { console.log(err); } return ( <div> {/* NextJS will render the component exported in loading.tsx here, intelligently swapping in DataTable whenever its data is ready */} <DataTable data={data} /> </div> ); } You can directly import the types generated by WunderGraph ( here; and you could even import if you wanted to type the API response itself!) – so you’re not missing out on any of the promised end-to-end type-safety, even if you aren’t using WunderGraph’s client-specific hooks. AlbumByIdResponseData AlbumByIdResponse This is what makes WunderGraph fully compatible with frameworks that React or Next.js. 💡 aren’t Also…would you look at that — . Thank you for recognizing that data fetching operations in webdev are inherently asynchronous operations, React! first class async/await support at a component level finally Option B: Manual Suspense Boundaries with **<Suspense>** Think you know better than Next.js and want to drive manual, stick shift and all? Then define your Suspense Boundaries yourself, for granular, component-level Streaming. No more . With this approach, your will need to import and set Suspense Boundaries manually for each piece of data-aware UI that needs to be rendered on this page (in our case, it’s just for ) loading.tsx [id].page/tsx <Suspense> <DataTable> import { Suspense } from "react"; import { AlbumByIdResponseData } from "../../../components/generated/models"; import DataTable from "../../../components/DataTable"; type Props = { params: Params; }; type Params = { id: string; }; export default async function AlbumPage(props: Props) { let data; let res; try { res = await fetch( `http://127.0.0.1:9991/app/main/operations/AlbumById?albumId=${props.params.id}` ); // If NodeJs fetch errors out using 'localhost', use '127.0.0.1' instead if (!res.ok) throw new Error(res.statusText); let json = await res.json(); data = json.data as AlbumByIdResponseData; } catch (err) { console.log(err); } return ( <div> <Suspense fallback={<p> Loading with manual Suspense Boundaries...</p>}> <DataTable data={data} /> </Suspense> </div> ); } Sticking with the default Server Component approach here. Remember, though, that unlike , 💡 loading.tsx defining manual Suspense Boundaries will delay navigation until after the new segment loads. I’m just using a simple here but you could, of course, define more complex fallback UIs – skeletons, spinners, custom images. Import and use them in fallback as you see fit! <p> Loading…</p> Finally, our component remains mostly unchanged — except for one fact. Our Form needs user interactivity, using hooks and browser APIs. So, if you’re extracting it out into its own component, explicitly make it a Client Component with Form “use client” "use client"; import React from "react"; import { useRouter } from "next/navigation"; type Props = {}; const AlbumForm = (props: Props) => { const router = useRouter(); const [albumId, setAlbumId] = React.useState("7"); // form submit handler function handleSubmit(event: React.FormEvent) { event.preventDefault(); router.push(`/albums/${albumId}`); } return ( <form onSubmit={handleSubmit} className="flex"> <label className="py-2 px-4 text-black bg-sky-700 text-lg font-bold rounded-l-xl">AlbumID</label> <input type="number" value={albumId} onChange={(event) => setAlbumId(event.target.value)} className="form-input w-32 py-2 px-4 text-black text-lg font-bold" /> <button type="submit" className="bg-sky-700 hover:bg-cyan-400 text-white text-lg font-bold py-2 px-4 ml-0 hover:border-blue-500 rounded-r-xl" > Search </button> </form> ); }; export default AlbumForm; Our can be reused as is. However, note that when using Dynamic Route Segments, you can pass URL params via props, . A good pattern to remember! DataTable effectively sharing state via the URL even if they’re Server Components Let’s close it out with the Navbar that we’ll use to switch between the Next.js 12 and 13 versions of what we just built. import React from 'react'; import Link from 'next/link' const Navbar = () => { return ( <nav className="bg-gradient-to-bl from-gray-700 via-gray-900 to-black py-2 px-4 flex items-center justify-between"> <div className="flex items-center"> <Link href="#" className="text-white font-bold text-xl"> AlbumViewr™ </Link> </div> <div className="flex items-center"> <Link href="/albums" className="text-white font-bold py-2 px-4 rounded-full hover:bg-gray-700"> Use NextJS 13 </Link> <Link href="/" className="text-white font-bold py-2 px-4 rounded-full hover:bg-gray-700 ml-4"> Use NextJS 12 </Link> </div> </nav> ); }; export default Navbar; Note Next 13’s new component! Much more intuitive, no longer needing a nested tag. <Link> <a> That’s All, Folks! Fire up your browser, head on over to , and try out the two versions of your app, swappable via the Navbar – one that uses old-school Next.js 12 and the pages directory, and one that uses Next.js 13, with Streaming SSR, Suspense, and a hybrid architecture of Server and Client Components – both with end-to-end type-safety enabled by as our API gateway/BFF. localhost:3000 WunderGraph Next.js 13 is not ready for production use yet — heck, even Vercel says so in their official Beta Next.js docs! But Vercel’s progress on Next.js 13 has been , its features incredibly useful for building awesome, user-friendly, accessible user experiences…and if you’re using WunderGraph, you can do all of that without compromising on type-safety, or the to boot! rapid developer experience Get hyped. We have some fun, days ahead as full-stack developers. fun Also Published Here