Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions examples/next-partial-prerendering/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
/.yarn

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# local env files
.env*
!.env*.example

# vercel
.vercel

# typescript
*.tsbuildinfo
3 changes: 3 additions & 0 deletions examples/next-partial-prerendering/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"singleQuote": true
}
23 changes: 23 additions & 0 deletions examples/next-partial-prerendering/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## Next.js Partial Prerendering

This is a demo of [Next.js](https://nextjs.org) using [Partial Prerendering](https://nextjs.org/docs/app/api-reference/next-config-js/partial-prerendering).

This template uses the new Next.js [App Router](https://nextjs.org/docs/app). This includes support for enhanced layouts, colocation of components, tests, and styles, component-level data fetching, and more.

It also uses the experimental Partial Prerendering feature available in Next.js 14. Partial Prerendering combines ultra-quick static edge delivery with fully dynamic capabilities and we believe it has the potential to [become the default rendering model for web applications](https://vercel.com/blog/partial-prerendering-with-next-js-creating-a-new-default-rendering-model), bringing together the best of static site generation and dynamic delivery.

> ⚠️ Please note that PPR is an experimental technology that is not recommended for production. You may run into some DX issues, especially on larger code bases.

## How it works

The index route `/` uses Partial Prerendering through:

1. Enabling the experimental flag in `next.config.js`.

```js
experimental: {
ppr: true,
},
```

2. Using `<Suspense />` to wrap Dynamic content.
Binary file not shown.
50 changes: 50 additions & 0 deletions examples/next-partial-prerendering/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { CartCountProvider } from '#/components/cart-count-context';
import { Header } from '#/components/header';
import { Sidebar } from '#/components/sidebar';
import { Metadata } from 'next';
import { GlobalStyles } from './styles';

export const metadata: Metadata = {
metadataBase: new URL('https://partialprerendering.com'),
title: 'Next.js Partial Prerendering',
description: 'A demo of Next.js using Partial Prerendering.',
openGraph: {
title: 'Next.js Partial Prerendering',
description: 'A demo of Next.js using Partial Prerendering.',
},
twitter: {
card: 'summary_large_image',
},
};

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className={`[color-scheme:dark]`}>
<head>
<GlobalStyles />
</head>
<body className="overflow-y-scroll bg-gray-1100 bg-[url('/grid.svg')] pb-36">
<Sidebar />
<div className="lg:pl-72">
<div className="mx-auto max-w-4xl space-y-8 px-2 pt-20 lg:px-8 lg:py-8">
<div className="rounded-lg bg-vc-border-gradient p-px shadow-lg shadow-black/20">
<div className="rounded-lg bg-black p-3.5 lg:p-6">
<CartCountProvider>
<div className="space-y-10">
<Header />

{children}
</div>
</CartCountProvider>
</div>
</div>
</div>
</div>
</body>
</html>
);
}
8 changes: 8 additions & 0 deletions examples/next-partial-prerendering/app/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default function NotFound() {
return (
<div className="space-y-4 text-vercel-pink">
<h2 className="text-lg font-bold">Not Found</h2>
<p className="text-sm">Could not find requested resource</p>
</div>
);
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 28 additions & 0 deletions examples/next-partial-prerendering/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Suspense } from 'react';
import {
RecommendedProducts,
RecommendedProductsSkeleton,
} from '#/components/recommended-products';
import { Reviews, ReviewsSkeleton } from '#/components/reviews';
import { SingleProduct } from '#/components/single-product';
import { Ping } from '#/components/ping';

export default function Page() {
return (
<div className="space-y-8 lg:space-y-14">
<SingleProduct />

<Ping />

<Suspense fallback={<RecommendedProductsSkeleton />}>
<RecommendedProducts />
</Suspense>

<Ping />

<Suspense fallback={<ReviewsSkeleton />}>
<Reviews />
</Suspense>
</div>
);
}
13 changes: 13 additions & 0 deletions examples/next-partial-prerendering/app/styles.tsx

Large diffs are not rendered by default.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
56 changes: 56 additions & 0 deletions examples/next-partial-prerendering/components/add-to-cart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'use client';

import { useRouter } from 'next/navigation';
import { useTransition } from 'react';
import { useCartCount } from '#/components/cart-count-context';

export function AddToCart({ initialCartCount }: { initialCartCount: number }) {
const router = useRouter();
const [isPending, startTransition] = useTransition();

const [, setOptimisticCartCount] = useCartCount(initialCartCount);

const addToCart = () => {
setOptimisticCartCount(initialCartCount + 1);

// update the cart count cookie
document.cookie = `_cart_count=${initialCartCount + 1}; path=/; max-age=${
60 * 60 * 24 * 30
}};`;

// Normally you would also send a request to the server to add the item
// to the current users cart
// await fetch(`https://api.acme.com/...`);

// Use a transition and isPending to create inline loading UI
startTransition(() => {
setOptimisticCartCount(null);

// Refresh the current route and fetch new data from the server without
// losing client-side browser or React state.
router.refresh();

// We're working on more fine-grained data mutation and revalidation:
// https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions
});
};

return (
<button
className="relative w-full items-center space-x-2 rounded-lg bg-vercel-blue px-3 py-1 text-sm font-medium text-white hover:bg-vercel-blue/90 disabled:text-white/70"
onClick={addToCart}
disabled={isPending}
>
Add to Cart
{isPending ? (
<div className="absolute right-2 top-1.5" role="status">
<div
className="
h-4 w-4 animate-spin rounded-full border-[3px] border-white border-r-transparent"
/>
<span className="sr-only">Loading...</span>
</div>
) : null}
</button>
);
}
31 changes: 31 additions & 0 deletions examples/next-partial-prerendering/components/byline.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { VercelLogo } from '#/components/vercel-logo';

export default function Byline({ className }: { className: string }) {
return (
<div
className={`${className} inset-x-0 bottom-3 mx-3 rounded-lg bg-vc-border-gradient p-px shadow-lg shadow-black/20`}
>
<div className="flex flex-row justify-between rounded-lg bg-black p-3.5 lg:px-5 lg:py-3">
<div className="flex items-center gap-x-1.5">
<div className="text-sm text-gray-400">By</div>
<a href="https://vercel.com" title="Vercel">
<div className="w-16 text-gray-100 hover:text-gray-50">
<VercelLogo />
</div>
</a>
</div>

<div className="text-sm text-gray-400">
<a
className="underline decoration-dotted underline-offset-4 transition-colors hover:text-gray-300"
href="https://github.com/vercel-labs/next-partial-prerendering"
target="_blank"
rel="noreferrer"
>
View code
</a>
</div>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use client';

import React, { useState } from 'react';

const CartCountContext = React.createContext<
| [null | number, React.Dispatch<React.SetStateAction<null | number>>]
| undefined
>(undefined);

export function CartCountProvider({ children }: { children: React.ReactNode }) {
const [optimisticCartCount, setOptimisticCartCount] = useState<null | number>(
null,
);

return (
<CartCountContext.Provider
value={[optimisticCartCount, setOptimisticCartCount]}
>
{children}
</CartCountContext.Provider>
);
}

export function useCartCount(
initialCount: number,
): [null | number, React.Dispatch<React.SetStateAction<null | number>>] {
const context = React.useContext(CartCountContext);
if (context === undefined) {
throw new Error('useCartCount must be used within a CartCountProvider');
}
if (context[0] === null) {
return [initialCount, context[1]];
}
return context;
}
8 changes: 8 additions & 0 deletions examples/next-partial-prerendering/components/cart-count.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use client';

import { useCartCount } from '#/components/cart-count-context';

export function CartCount({ initialCartCount }: { initialCartCount: number }) {
const [count] = useCartCount(initialCartCount);
return <span>{count}</span>;
}
60 changes: 60 additions & 0 deletions examples/next-partial-prerendering/components/header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { NextLogo } from '#/components/next-logo';
import {
MagnifyingGlassIcon,
ShoppingCartIcon,
} from '@heroicons/react/24/solid';
import Image from 'next/image';
import { CartCount } from '#/components/cart-count';
import { cookies } from 'next/headers';
import { Suspense } from 'react';

async function CartCountFromCookies() {
const cartCount = Number(cookies().get('_cart_count')?.value || '0');
return <CartCount initialCartCount={cartCount} />;
}

export function Header() {
return (
<div className="flex items-center justify-between gap-x-3 rounded-lg bg-gray-800 px-3 py-3 lg:px-5 lg:py-4">
<div className="flex gap-x-3">
<div className="h-10 w-10 hover:opacity-70">
<NextLogo />
</div>

<div className="relative flex-1">
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<MagnifyingGlassIcon className="h-5 w-5 text-gray-300" />
</div>
<input
aria-label="Search"
type="search"
name="search"
id="search"
className="block w-full rounded-full border-none bg-gray-600 pl-10 font-medium text-gray-200 focus:border-vercel-pink focus:ring-2 focus:ring-vercel-pink"
autoComplete="off"
/>
</div>
</div>

<div className="flex shrink-0 gap-x-3">
<div className="relative flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-gray-600 text-white">
<ShoppingCartIcon className="w-6 text-white" />
<div className="absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center rounded-full bg-vercel-cyan text-sm font-bold text-cyan-800">
<Suspense fallback={<span></span>}>
<CartCountFromCookies />
</Suspense>
</div>
</div>

<Image
src="/prince-akachi-LWkFHEGpleE-unsplash.jpg"
className="rounded-full"
width={40}
height={40}
alt="User"
priority
/>
</div>
</div>
);
}
Loading