Next.js 13 Nested Layouts, Streaming SSR with Realtime GraphQL
- Nested Layouts
- Data Fetching
- Streaming and Suspense
- Client and Server Components
Next.js Setup
npx create-next-app@latest --experimental-app
npm install @graphql-codegen/cli @graphql-codegen/client-preset @graphql-typed-document-node/core dotenv graphql graphql-request graphql-ws
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
appDir: true,
runtime: "experimental-edge",
},
images: {
remotePatterns: [
{
protocol: "http",
hostname: "img6a.flixcart.com",
},
{
protocol: "http",
hostname: "img5a.flixcart.com",
},
],
},
};
module.exports = nextConfig;
NEXT_PUBLIC_HASURA_GRAPHQL_URL=<Hasura GraphQL endpoint>
NEXT_PUBLIC_HASURA_GRAPHQL_WS_URL=<Hasura websocket endpoint>
GraphQL Code Generator Setup
import type { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {
overwrite: true,
schema: [
{
[process.env.NEXT_PUBLIC_HASURA_GRAPHQL_URL!]: {
headers: {
// If you have an admin secret set
"x-hasura-admin-secret": process.env.HASURA_GRAPHQL_ADMIN_SECRET!,
},
},
},
],
config: {
skipTypename: true,
enumsAsTypes: true,
scalars: {
numeric: "number",
},
},
documents: "lib/service/queries.graphql",
generates: {
"lib/gql/": {
preset: "client",
config: {},
plugins: [],
},
},
};
export default config;
query GetProducts {
product(limit: 10) {
id
name
}
}
query GetProduct($id: Int!) {
product_by_pk(id: $id) {
id
description
name
price
image_urls(path: "$[0]")
}
}
mutation PlaceOrder($products: order_product_arr_rel_insert_input!) {
insert_order_one(
object: {
products: $products
billing_address_id: 222
user_id: 225
shipping_address_id: 222
}
) {
id
}
}
npm run codegen
graphql-request Setup
import { GraphQLClient } from "graphql-request";
export const gqlClient = new GraphQLClient(
process.env.NEXT_PUBLIC_HASURA_GRAPHQL_URL!,
{ fetch }
);
Nested Layouts
Fetch Home Page Data
import Link from "next/link";
import { GetProductsDocument } from "../lib/gql/graphql";
import { gqlClient } from "../lib/service/client";
import "./globals.css";
async function getProducts() {
const { product: products } = await gqlClient.request(
GetProductsDocument,
{}
);
return products;
}
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const products = await getProducts();
return (
<html lang="en">
<head />
<body>
<main style={{ display: "flex" }}>
<section style={{ minWidth: "400px", width: "400px" }}>
<ul>
{products.map((product) => (
<li key={product.id}>
<Link href={`/${product.id}`}>{product.name}</Link>
</li>
))}
</ul>
</section>
<section style={{ flexGrow: 1 }}>{children}</section>
</main>
</body>
</html>
);
}
Product Details Page
import Image from "next/image";
import { GetProductDocument } from "../../lib/gql/graphql";
import { gqlClient } from "../../lib/service/client";
async function getProduct(id: number) {
const { product_by_pk } = await gqlClient.request(GetProductDocument, { id });
return product_by_pk!;
}
export default async function Page({
params: { id },
}: {
params: { id: string };
}) {
const product = await getProduct(Number(id));
return (
<>
<h1>{product.name}</h1>
<Image
src={product.image_urls}
alt={`${product.name} Picture`}
width={200}
height={200}
></Image>
<div>{`$${product.price.toFixed(2)}`}</div>
<p>{product.description}</p>
</>
);
}
Streaming and Suspense
export default function Loading() {
// You can add any UI inside Loading, including a Skeleton.
return <div>loading</div>;
}
Client Components
"use client";
import { useEffect, useState } from "react";
import { PlaceOrderDocument } from "../../lib/gql/graphql";
import { createClient } from "graphql-ws";
import { gqlClient } from "../../lib/service/client";
const wsClient = createClient({
url: process.env.NEXT_PUBLIC_HASURA_GRAPHQL_WS_URL!,
});
export default function Orders({ id }: { id: number }) {
const [orders, setOrders] = useState<
Array<{ id: number; created_at: string; quantity: number }>
>([]);
useEffect(() => {
const unsubscribe = wsClient.subscribe(
{
query: `
subscription ProductOrders($id: Int!) {
order_product_stream(
batch_size: 10
cursor: { initial_value: { created_at: "2021-02-22T18:16:12.95499+00:00" } }
where: { product_id: { _eq: $id } }
) {
id
quantity
created_at
}
}`,
variables: {
id,
},
},
{
next: ({ data }) => {
setOrders((orders) => [
...orders,
...(data?.order_product_stream as any),
]);
},
error: (error) => {
console.log(error);
},
complete: () => {
console.log("complete");
},
}
);
return () => {
unsubscribe();
};
}, [id]);
return (
<>
<button
onClick={async () =>
await gqlClient.request(PlaceOrderDocument, {
products: { data: [{ product_id: id, quantity: 1 }] },
})
}
>
Order
</button>
<li>
{orders.map((order) => (
<li key={order.id}>
{`Order ${order.id} created at ${new Date(order.created_at)}`}
</li>
))}
</li>
</>
);
}
import Orders from "../components/orders";
export default async function Page({
params: { id },
}: {
params: { id: string };
}) {
const product = await getProduct(Number(id));
return (
<>
...
<Orders id={Number(id)}></Orders>
</>
);
}
Conclusion
Related reading