Skip to content

Commit 156d9f1

Browse files
authored
Import the next-partial-prerendering example (#308)
https://github.com/vercel-labs/next-partial-prerendering
1 parent 68c7d6d commit 156d9f1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+1869
-34
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
/.yarn
8+
9+
# testing
10+
/coverage
11+
12+
# next.js
13+
/.next/
14+
/out/
15+
16+
# production
17+
/build
18+
19+
# misc
20+
.DS_Store
21+
*.pem
22+
23+
# debug
24+
npm-debug.log*
25+
yarn-debug.log*
26+
yarn-error.log*
27+
.pnpm-debug.log*
28+
29+
# local env files
30+
.env*
31+
!.env*.example
32+
33+
# vercel
34+
.vercel
35+
36+
# typescript
37+
*.tsbuildinfo
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"singleQuote": true
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
## Next.js Partial Prerendering
2+
3+
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).
4+
5+
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.
6+
7+
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.
8+
9+
> ⚠️ 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.
10+
11+
## How it works
12+
13+
The index route `/` uses Partial Prerendering through:
14+
15+
1. Enabling the experimental flag in `next.config.js`.
16+
17+
```js
18+
experimental: {
19+
ppr: true,
20+
},
21+
```
22+
23+
2. Using `<Suspense />` to wrap Dynamic content.
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { CartCountProvider } from '#/components/cart-count-context';
2+
import { Header } from '#/components/header';
3+
import { Sidebar } from '#/components/sidebar';
4+
import { Metadata } from 'next';
5+
import { GlobalStyles } from './styles';
6+
7+
export const metadata: Metadata = {
8+
metadataBase: new URL('https://partialprerendering.com'),
9+
title: 'Next.js Partial Prerendering',
10+
description: 'A demo of Next.js using Partial Prerendering.',
11+
openGraph: {
12+
title: 'Next.js Partial Prerendering',
13+
description: 'A demo of Next.js using Partial Prerendering.',
14+
},
15+
twitter: {
16+
card: 'summary_large_image',
17+
},
18+
};
19+
20+
export default function RootLayout({
21+
children,
22+
}: {
23+
children: React.ReactNode;
24+
}) {
25+
return (
26+
<html lang="en" className={`[color-scheme:dark]`}>
27+
<head>
28+
<GlobalStyles />
29+
</head>
30+
<body className="overflow-y-scroll bg-gray-1100 bg-[url('/grid.svg')] pb-36">
31+
<Sidebar />
32+
<div className="lg:pl-72">
33+
<div className="mx-auto max-w-4xl space-y-8 px-2 pt-20 lg:px-8 lg:py-8">
34+
<div className="rounded-lg bg-vc-border-gradient p-px shadow-lg shadow-black/20">
35+
<div className="rounded-lg bg-black p-3.5 lg:p-6">
36+
<CartCountProvider>
37+
<div className="space-y-10">
38+
<Header />
39+
40+
{children}
41+
</div>
42+
</CartCountProvider>
43+
</div>
44+
</div>
45+
</div>
46+
</div>
47+
</body>
48+
</html>
49+
);
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export default function NotFound() {
2+
return (
3+
<div className="space-y-4 text-vercel-pink">
4+
<h2 className="text-lg font-bold">Not Found</h2>
5+
<p className="text-sm">Could not find requested resource</p>
6+
</div>
7+
);
8+
}
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Suspense } from 'react';
2+
import {
3+
RecommendedProducts,
4+
RecommendedProductsSkeleton,
5+
} from '#/components/recommended-products';
6+
import { Reviews, ReviewsSkeleton } from '#/components/reviews';
7+
import { SingleProduct } from '#/components/single-product';
8+
import { Ping } from '#/components/ping';
9+
10+
export default function Page() {
11+
return (
12+
<div className="space-y-8 lg:space-y-14">
13+
<SingleProduct />
14+
15+
<Ping />
16+
17+
<Suspense fallback={<RecommendedProductsSkeleton />}>
18+
<RecommendedProducts />
19+
</Suspense>
20+
21+
<Ping />
22+
23+
<Suspense fallback={<ReviewsSkeleton />}>
24+
<Reviews />
25+
</Suspense>
26+
</div>
27+
);
28+
}

examples/next-partial-prerendering/app/styles.tsx

+13
Large diffs are not rendered by default.
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
'use client';
2+
3+
import { useRouter } from 'next/navigation';
4+
import { useTransition } from 'react';
5+
import { useCartCount } from '#/components/cart-count-context';
6+
7+
export function AddToCart({ initialCartCount }: { initialCartCount: number }) {
8+
const router = useRouter();
9+
const [isPending, startTransition] = useTransition();
10+
11+
const [, setOptimisticCartCount] = useCartCount(initialCartCount);
12+
13+
const addToCart = () => {
14+
setOptimisticCartCount(initialCartCount + 1);
15+
16+
// update the cart count cookie
17+
document.cookie = `_cart_count=${initialCartCount + 1}; path=/; max-age=${
18+
60 * 60 * 24 * 30
19+
}};`;
20+
21+
// Normally you would also send a request to the server to add the item
22+
// to the current users cart
23+
// await fetch(`https://api.acme.com/...`);
24+
25+
// Use a transition and isPending to create inline loading UI
26+
startTransition(() => {
27+
setOptimisticCartCount(null);
28+
29+
// Refresh the current route and fetch new data from the server without
30+
// losing client-side browser or React state.
31+
router.refresh();
32+
33+
// We're working on more fine-grained data mutation and revalidation:
34+
// https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions
35+
});
36+
};
37+
38+
return (
39+
<button
40+
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"
41+
onClick={addToCart}
42+
disabled={isPending}
43+
>
44+
Add to Cart
45+
{isPending ? (
46+
<div className="absolute right-2 top-1.5" role="status">
47+
<div
48+
className="
49+
h-4 w-4 animate-spin rounded-full border-[3px] border-white border-r-transparent"
50+
/>
51+
<span className="sr-only">Loading...</span>
52+
</div>
53+
) : null}
54+
</button>
55+
);
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { VercelLogo } from '#/components/vercel-logo';
2+
3+
export default function Byline({ className }: { className: string }) {
4+
return (
5+
<div
6+
className={`${className} inset-x-0 bottom-3 mx-3 rounded-lg bg-vc-border-gradient p-px shadow-lg shadow-black/20`}
7+
>
8+
<div className="flex flex-row justify-between rounded-lg bg-black p-3.5 lg:px-5 lg:py-3">
9+
<div className="flex items-center gap-x-1.5">
10+
<div className="text-sm text-gray-400">By</div>
11+
<a href="https://vercel.com" title="Vercel">
12+
<div className="w-16 text-gray-100 hover:text-gray-50">
13+
<VercelLogo />
14+
</div>
15+
</a>
16+
</div>
17+
18+
<div className="text-sm text-gray-400">
19+
<a
20+
className="underline decoration-dotted underline-offset-4 transition-colors hover:text-gray-300"
21+
href="https://github.com/vercel-labs/next-partial-prerendering"
22+
target="_blank"
23+
rel="noreferrer"
24+
>
25+
View code
26+
</a>
27+
</div>
28+
</div>
29+
</div>
30+
);
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use client';
2+
3+
import React, { useState } from 'react';
4+
5+
const CartCountContext = React.createContext<
6+
| [null | number, React.Dispatch<React.SetStateAction<null | number>>]
7+
| undefined
8+
>(undefined);
9+
10+
export function CartCountProvider({ children }: { children: React.ReactNode }) {
11+
const [optimisticCartCount, setOptimisticCartCount] = useState<null | number>(
12+
null,
13+
);
14+
15+
return (
16+
<CartCountContext.Provider
17+
value={[optimisticCartCount, setOptimisticCartCount]}
18+
>
19+
{children}
20+
</CartCountContext.Provider>
21+
);
22+
}
23+
24+
export function useCartCount(
25+
initialCount: number,
26+
): [null | number, React.Dispatch<React.SetStateAction<null | number>>] {
27+
const context = React.useContext(CartCountContext);
28+
if (context === undefined) {
29+
throw new Error('useCartCount must be used within a CartCountProvider');
30+
}
31+
if (context[0] === null) {
32+
return [initialCount, context[1]];
33+
}
34+
return context;
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
'use client';
2+
3+
import { useCartCount } from '#/components/cart-count-context';
4+
5+
export function CartCount({ initialCartCount }: { initialCartCount: number }) {
6+
const [count] = useCartCount(initialCartCount);
7+
return <span>{count}</span>;
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { NextLogo } from '#/components/next-logo';
2+
import {
3+
MagnifyingGlassIcon,
4+
ShoppingCartIcon,
5+
} from '@heroicons/react/24/solid';
6+
import Image from 'next/image';
7+
import { CartCount } from '#/components/cart-count';
8+
import { cookies } from 'next/headers';
9+
import { Suspense } from 'react';
10+
11+
async function CartCountFromCookies() {
12+
const cartCount = Number(cookies().get('_cart_count')?.value || '0');
13+
return <CartCount initialCartCount={cartCount} />;
14+
}
15+
16+
export function Header() {
17+
return (
18+
<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">
19+
<div className="flex gap-x-3">
20+
<div className="h-10 w-10 hover:opacity-70">
21+
<NextLogo />
22+
</div>
23+
24+
<div className="relative flex-1">
25+
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
26+
<MagnifyingGlassIcon className="h-5 w-5 text-gray-300" />
27+
</div>
28+
<input
29+
aria-label="Search"
30+
type="search"
31+
name="search"
32+
id="search"
33+
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"
34+
autoComplete="off"
35+
/>
36+
</div>
37+
</div>
38+
39+
<div className="flex shrink-0 gap-x-3">
40+
<div className="relative flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-gray-600 text-white">
41+
<ShoppingCartIcon className="w-6 text-white" />
42+
<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">
43+
<Suspense fallback={<span></span>}>
44+
<CartCountFromCookies />
45+
</Suspense>
46+
</div>
47+
</div>
48+
49+
<Image
50+
src="/prince-akachi-LWkFHEGpleE-unsplash.jpg"
51+
className="rounded-full"
52+
width={40}
53+
height={40}
54+
alt="User"
55+
priority
56+
/>
57+
</div>
58+
</div>
59+
);
60+
}

0 commit comments

Comments
 (0)