Skip to content

Commit

Permalink
Suspense fallbacks example (#14)
Browse files Browse the repository at this point in the history
* ppr shells

* rename
  • Loading branch information
dferber90 authored Jan 10, 2025
1 parent 9c9c7ac commit 3db88e1
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 0 deletions.
127 changes: 127 additions & 0 deletions examples/snippets/app/examples/suspense-fallbacks/[code]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { coreFlags, hasAuthCookieFlag } from '../flags';
import { cookies, headers } from 'next/headers';
import Image from 'next/image';
import { generatePermutations } from '@vercel/flags/next';
import { Suspense } from 'react';

// prerender this page for all permutations of the flags
export async function generateStaticParams() {
const permutations = await generatePermutations(coreFlags);
return permutations.map((code) => ({ code }));
}

const delay = (ms = 700) =>
new Promise((resolve) => {
setTimeout(resolve, ms);
});

const shimmer = `relative overflow-hidden before:absolute before:inset-0 before:-translate-x-full before:animate-[shimmer_1.5s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/10 before:to-transparent`;

function AuthedUserSkeleton() {
return (
<div className="flex flex-row items-center gap-2">
<div className="relative flex h-10 w-10 shrink-0 items-center justify-center rounded-full text-white">
<div className={`h-10 w-10 rounded-full bg-gray-200 ${shimmer}`} />
</div>

<button
type="submit"
disabled
className={`relative h-10 w-full items-center space-x-2 rounded-lg bg-gray-200 px-3 py-1 text-sm font-medium text-gray-200 ${shimmer}`}
>
Sign out
</button>
</div>
);
}

function AnonUser() {
return (
<form
action={async function signIn() {
'use server';
const jar = await cookies();
jar.set('suspense-fallbacks-user-id', '1', {
maxAge: 1000 * 60 * 60,
});
}}
>
<button
type="submit"
className="relative h-10 w-full items-center space-x-2 rounded-lg bg-black px-3 py-1 text-sm font-medium text-white hover:bg-vercel-blue/90 disabled:text-white/70"
>
Sign in
</button>
</form>
);
}

async function User() {
const headersStore = await headers();
const cookieStore = await cookies();

// artificially delay the auth state to simulate resolving the user's auth,
// but only if we are not hanlding a server action
if (!headersStore.has('next-action')) await delay();

if (cookieStore.get('suspense-fallbacks-user-id')?.value !== '1')
return <AnonUser />;

return (
<div className="flex flex-row items-center gap-2">
<div>
<Image
src="/prince-akachi-LWkFHEGpleE-unsplash.jpg"
className="rounded-full m-0"
width={40}
height={40}
alt="User"
priority
/>
</div>

<form
action={async function signOut() {
'use server';
const jar = await cookies();
jar.delete('suspense-fallbacks-user-id');
}}
>
<button
type="submit"
className="relative h-10 w-full items-center space-x-2 rounded-lg bg-black px-3 py-1 text-sm font-medium text-white hover:bg-vercel-blue/90 disabled:text-white/70"
>
Sign out
</button>
</form>
</div>
);
}

export default async function Page({
params,
}: {
params: Promise<{ code: string }>;
}) {
const awaitedParams = await params;
const hasAuthCookie = await hasAuthCookieFlag(awaitedParams.code, coreFlags);

return (
<>
<div className="w-full max-w-4xl mx-auto p-4 bg-gray-100 shadow-md rounded-lg my-4">
<div className="flex justify-between items-center">
<div className="text-xl font-semibold text-primary">My App</div>
<Suspense
fallback={hasAuthCookie ? <AuthedUserSkeleton /> : <AnonUser />}
>
<User />
</Suspense>
</div>
</div>
<p className="italic">
Reload this page while signed in or signed out to see that the loading
skeleton matches the predicted auth state.
</p>
</>
);
}
10 changes: 10 additions & 0 deletions examples/snippets/app/examples/suspense-fallbacks/flags.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { flag } from '@vercel/flags/next';

export const hasAuthCookieFlag = flag<boolean>({
key: 'has-auth-cookie',
decide({ cookies }) {
return cookies.has('suspense-fallbacks-user-id');
},
});

export const coreFlags = [hasAuthCookieFlag];
13 changes: 13 additions & 0 deletions examples/snippets/app/examples/suspense-fallbacks/middleware.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { precompute } from '@vercel/flags/next';
import { type NextRequest, NextResponse } from 'next/server';
import { coreFlags } from './flags';

export async function pprShellsMiddleware(request: NextRequest) {
// precompute the flags
const code = await precompute(coreFlags);

// rewrite the page with the code
return NextResponse.rewrite(
new URL(`/examples/suspense-fallbacks/${code}`, request.url),
);
}
5 changes: 5 additions & 0 deletions examples/snippets/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ export default function Page() {
description="Using feature flags in Pages Router on static pages"
href="/examples/pages-router-precomputed"
/>
<ConceptCard
title="Suspense Fallbacks"
description="Using prerendered suspense fallbacks"
href="/examples/suspense-fallbacks"
/>
</div>
</Content>
);
Expand Down
5 changes: 5 additions & 0 deletions examples/snippets/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { NextRequest } from 'next/server';
import { marketingMiddleware } from './app/examples/marketing-pages/middleware';
import { overviewMiddleware } from './app/getting-started/overview/[code]/middleware';
import { featureFlagsInEdgeMiddleware } from './app/examples/feature-flags-in-edge-middleware/middleware';
import { pprShellsMiddleware } from './app/examples/suspense-fallbacks/middleware';
import { manualPrecomputeMiddleware } from './app/concepts/precompute/manual/middleware';
import { automaticPrecomputeMiddleware } from './app/concepts/precompute/automatic/[code]/middleware';
import { pagesRouterMiddleware } from './lib/pages-router-precomputed/middleware';
Expand Down Expand Up @@ -33,6 +34,9 @@ export function middleware(request: NextRequest) {
if (request.nextUrl.pathname === '/examples/pages-router-precomputed') {
return pagesRouterMiddleware(request);
}
if (request.nextUrl.pathname === '/examples/suspense-fallbacks') {
return pprShellsMiddleware(request);
}

return NextResponse.next();
}
Expand All @@ -45,5 +49,6 @@ export const config = {
'/examples/marketing-pages',
'/examples/feature-flags-in-edge-middleware',
'/examples/pages-router-precomputed',
'/examples/suspense-fallbacks',
],
};
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions examples/snippets/tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ export default {
],
theme: {
extend: {
keyframes: () => ({
shimmer: {
'100%': {
transform: 'translateX(100%)',
},
},
}),
fontFamily: {
sans: ['var(--font-geist-sans)'],
mono: ['var(--font-geist-mono)'],
Expand Down

0 comments on commit 3db88e1

Please sign in to comment.