├── .gitignore ├── .vscode └── settings.json ├── README.md ├── app ├── favicon.ico ├── layout.tsx └── new │ ├── page.tsx │ └── settings │ ├── display │ └── page.tsx │ ├── layout.tsx │ └── profile │ └── page.tsx ├── components ├── cards.tsx ├── home-layout.tsx ├── minimal.tsx ├── navbar.tsx ├── settings-layout.tsx └── table.tsx ├── lib └── db.ts ├── next.config.js ├── package.json ├── pages ├── _app.tsx ├── _document.tsx ├── api │ └── submit.ts ├── index.tsx └── settings │ ├── display.tsx │ └── profile.tsx ├── pnpm-lock.yaml ├── postcss.config.js ├── styles └── globals.css ├── tailwind.config.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 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 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js Metamorphosis (React Summit 2023) 2 | 3 | [🎥 **Watch Talk**](https://www.youtube.com/watch?v=5HaX0Q_Do1I) 4 | 5 | This was the source for my talk about React Summit 2023. In the talk, I show how to incrementally adopt to new App Router, and some of the benefits of using the new router versus the previous Pages Router approach. 6 | 7 | This includes: 8 | 9 | - Simplified data fetching 10 | 1. `getInitialProps` (Pages Router) 11 | 1. `getStaticProps` / `getServerSideProps` (Pages Router) 12 | 1. `async` / `await` (App Router) 13 | - Colocating data fetching with components (React Server Components) 14 | - Defining layouts through the file system (`layout.tsx`) 15 | - Granular control over data freshness (`revalidate` for components) 16 | - Out-of-order streaming of data (Suspense for SSR) 17 | - Simplified form submissions (Server Actions) 18 | 19 | ## `NEXT_DATA` vs RSC Payload 20 | 21 | The data fetching methods in the Pages Router, like `getServerSideProps`, forward `props` to the default exported React component. This `NEXT_DATA` payload also includes rendering instructions. 22 | 23 | Inside the App Router, there is no longer `NEXT_DATA`. Instead, the React Server Components payload includes the already rendered result, meaning no additional rendering work needs to be done on the client. This also means that the rendering instructions remain on the server, and just need to be "slotted" into the right place. 24 | 25 | You'll also notice that in the App Router, the baseline client-side JavaScript of Next.js + React is smaller than the Pages Router. 26 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leerob/nextjs-metamorphosis/a0b60b58af01414888ee99721b48066f21ec5014/app/favicon.ico -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import 'styles/globals.css'; 2 | import Navbar from 'components/navbar'; 3 | 4 | export default function RootLayout({ 5 | children, 6 | }: { 7 | children: React.ReactNode; 8 | }) { 9 | return ( 10 | 11 | 12 | 13 | {children} 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /app/new/page.tsx: -------------------------------------------------------------------------------- 1 | import Cards from 'components/cards'; 2 | import Layout from 'components/home-layout'; 3 | import Table from 'components/table'; 4 | import { Suspense } from 'react'; 5 | 6 | export default async function HomePage() { 7 | return ( 8 | 9 | }> 10 | 11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /app/new/settings/display/page.tsx: -------------------------------------------------------------------------------- 1 | import MinimalMode from 'components/minimal'; 2 | import Table from 'components/table'; 3 | 4 | export default function DisplayPage() { 5 | return ( 6 | 7 |
8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /app/new/settings/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Layout } from 'components/settings-layout'; 2 | 3 | export default function SettingsLayout({ 4 | children, 5 | }: { 6 | children: React.ReactNode; 7 | }) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /app/new/settings/profile/page.tsx: -------------------------------------------------------------------------------- 1 | import { Bio, Button, Name } from 'components/settings-layout'; 2 | 3 | export default function ProfilePage() { 4 | async function handleSubmit(formData: FormData) { 5 | 'use server'; 6 | const name = formData.get('name'); 7 | const bio = formData.get('bio'); 8 | console.log(name, bio); 9 | } 10 | 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /components/cards.tsx: -------------------------------------------------------------------------------- 1 | import { getPlaceholderCardData, getCardData, Card, Icon } from 'lib/db'; 2 | 3 | export default function Cards({ 4 | data, 5 | loading, 6 | slow, 7 | }: { 8 | data?: Card[]; 9 | loading?: boolean; 10 | slow?: boolean; 11 | }) { 12 | if (data === undefined) { 13 | data = getPlaceholderCardData(); 14 | } 15 | if (slow === true) { 16 | // data = await getCardData(2000); 17 | } 18 | 19 | return ( 20 |
21 | {data.map((card) => ( 22 | 23 | ))} 24 |
25 | ); 26 | } 27 | 28 | function Card({ card, isLoading }: { card: any; isLoading?: boolean }) { 29 | const getIcon = (icon: Icon) => { 30 | switch (icon) { 31 | case Icon.Revenue: 32 | return ( 33 | 45 | 46 | 47 | 48 | ); 49 | case Icon.Subscriptions: 50 | return ( 51 | 63 | 64 | 65 | 66 | 67 | 68 | ); 69 | case Icon.Sales: 70 | return ( 71 | 83 | 84 | 85 | 86 | ); 87 | case Icon.ActiveNow: 88 | return ( 89 | 101 | 102 | 103 | ); 104 | default: 105 | return null; 106 | } 107 | }; 108 | 109 | return ( 110 |
111 |
112 | {isLoading ? ( 113 |
114 | ) : ( 115 | <> 116 |

{card.title}

117 | {getIcon(card.icon)} 118 | 119 | )} 120 |
121 |
122 | {isLoading ? ( 123 | <> 124 |
125 |
126 | 127 | ) : ( 128 | <> 129 |
{card.value}
130 |

{card.description}

131 | 132 | )} 133 |
134 |
135 | ); 136 | } 137 | -------------------------------------------------------------------------------- /components/home-layout.tsx: -------------------------------------------------------------------------------- 1 | import Navbar from './navbar'; 2 | 3 | export default function Layout({ children }: { children: React.ReactNode }) { 4 | return ( 5 | <> 6 | 7 |
8 |
9 |

Lee's Account

10 |

View your recent invoices.

11 |
12 |
17 | {children} 18 |
19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /components/minimal.tsx: -------------------------------------------------------------------------------- 1 | export default function MinimalMode({ 2 | children, 3 | }: { 4 | children: React.ReactNode; 5 | }) { 6 | return ( 7 | <> 8 |
9 |
10 | 13 |

14 | Display only the most important information for invoices. 15 |

16 |
17 | 23 |
24 |
25 |
{children}
26 |
27 |
28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /components/navbar.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | export default function Navbar() { 4 | return ( 5 |
6 | 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /components/settings-layout.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Navbar from './navbar'; 4 | import Link from 'next/link'; 5 | import { usePathname } from 'next/navigation'; 6 | 7 | function classNames(...classes: any) { 8 | return classes.filter(Boolean).join(' '); 9 | } 10 | 11 | export function Layout({ children }: { children: React.ReactNode }) { 12 | const pathname = usePathname() || ''; 13 | const settingsUrl = pathname.includes('/new') 14 | ? '/new/settings/profile' 15 | : '/settings/profile'; 16 | const displayUrl = pathname.includes('/new') 17 | ? '/new/settings/display' 18 | : '/settings/display'; 19 | 20 | return ( 21 | <> 22 | 23 |
24 |
25 |

Settings

26 |

27 | Manage your account settings and set e-mail preferences. 28 |

29 |
30 |
35 |
36 | 78 |
79 |
{children}
80 |
81 |
82 |
83 | 84 | ); 85 | } 86 | 87 | export function Name() { 88 | return ( 89 |
90 | 93 | 99 |

100 | This is your public display name. It can be your real name or a 101 | pseudonym. You can only change this once every 30 days. 102 |

103 |
104 | ); 105 | } 106 | 107 | export function Bio() { 108 | return ( 109 |
110 | 113 |