├── .prettierrc.json ├── app ├── loading.js ├── movies │ ├── page.js │ ├── layout.js │ └── [id] │ │ └── page.js ├── page.js ├── nav-link.js └── layout.js ├── postcss.config.js ├── .eslintrc.json ├── tailwind.config.js ├── next.config.js ├── README.md ├── .gitignore ├── package.json ├── db.json └── SCRIPT.md /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /app/loading.js: -------------------------------------------------------------------------------- 1 | export default function Loading() { 2 | return

Loading...

; 3 | } 4 | -------------------------------------------------------------------------------- /app/movies/page.js: -------------------------------------------------------------------------------- 1 | export default function Page() { 2 | return

Select a film!

; 3 | } 4 | -------------------------------------------------------------------------------- /app/page.js: -------------------------------------------------------------------------------- 1 | export default function Page() { 2 | return

Hello, Next.js Conf!

; 3 | } 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "next/core-web-vitals"], 3 | "rules": { 4 | "@next/next/no-head-element": "off", 5 | "react/no-unescaped-entities": "off" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./app/**/*.{js,ts,jsx,tsx}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: false, 4 | experimental: { 5 | appDir: true, 6 | newNextLinkBehavior: true, 7 | }, 8 | }; 9 | 10 | module.exports = nextConfig; 11 | -------------------------------------------------------------------------------- /app/nav-link.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | import { useSelectedLayoutSegment } from "next/navigation"; 5 | 6 | export default function NavLink({ href, children }) { 7 | let segment = useSelectedLayoutSegment(); 8 | let active = href === `/${segment}`; 9 | 10 | return ( 11 | 12 | {children} 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nested layouts by example 2 | 3 | Source code from my Next.js conf talk about nested layouts in Next.js. 4 | 5 | - [View the diff from the video](https://github.com/samselikoff/nextconf-2022-nested-layouts-by-example/commit/d57c0f11e62898b5527c8ba6e265f385c15e669d) 6 | 7 | ## Running the demo locally 8 | 9 | ```sh 10 | npm i 11 | 12 | # start the api 13 | npm run api 14 | 15 | # in another terminal, start the app 16 | npm run dev 17 | ``` 18 | 19 | Now visit localhost:3000. 20 | -------------------------------------------------------------------------------- /.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 | /cypress/videos/* 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*.local 31 | .env.test 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | next-env.d.ts 39 | -------------------------------------------------------------------------------- /app/layout.js: -------------------------------------------------------------------------------- 1 | import "tailwindcss/tailwind.css"; 2 | import NavLink from "./nav-link"; 3 | 4 | export default function RootLayout({ children }) { 5 | return ( 6 | 7 | 8 | Nested layouts by example 9 | 10 | 11 | 12 |
13 | 17 |
18 | 19 |
{children}
20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dev": "next dev", 4 | "build": "next build", 5 | "start": "next start", 6 | "lint": "next lint", 7 | "api": "json-server --watch db.json --port 3001" 8 | }, 9 | "dependencies": { 10 | "framer-motion": "^7.5.3", 11 | "next": "12.3.2-canary.28", 12 | "react": "0.0.0-experimental-3b814327e-20221014", 13 | "react-dom": "0.0.0-experimental-3b814327e-20221014" 14 | }, 15 | "devDependencies": { 16 | "autoprefixer": "^10.4.12", 17 | "eslint": "8.24.0", 18 | "eslint-config-next": "12.3.1", 19 | "postcss": "^8.4.17", 20 | "prettier": "2.7.1", 21 | "tailwindcss": "^3.1.8" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/movies/layout.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { experimental_use as use } from "react"; 3 | 4 | async function getMovies() { 5 | let res = await fetch("http://localhost:3001/movies"); 6 | 7 | return res.json(); 8 | } 9 | 10 | export default function Layout({ children }) { 11 | let movies = use(getMovies()); 12 | 13 | return ( 14 |
15 | 22 | 23 |
{children}
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /app/movies/[id]/page.js: -------------------------------------------------------------------------------- 1 | import { experimental_use as use } from "react"; 2 | 3 | async function getMovie(id) { 4 | let res = await fetch(`http://localhost:3001/movies/${id}`); 5 | 6 | return res.json(); 7 | } 8 | 9 | export default function Page({ params }) { 10 | let movie = use(getMovie(params.id)); 11 | 12 | return ( 13 |
14 |

{movie.title}

15 |

Year: {movie.year}

16 |

{movie.description}

17 |
18 | ); 19 | } 20 | 21 | export async function generateStaticParams() { 22 | let res = await fetch("http://localhost:3001/movies"); 23 | let movies = await res.json(); 24 | 25 | return movies.map((movie) => ({ id: movie.id })); 26 | } 27 | -------------------------------------------------------------------------------- /db.json: -------------------------------------------------------------------------------- 1 | { 2 | "movies": [ 3 | { 4 | "id": "1", 5 | "title": "Lord of the Rings", 6 | "year": 2001, 7 | "description": "A meek Hobbit from the Shire and eight companions set out on a journey to destroy the powerful One Ring and save Middle-earth from the Dark Lord Sauron." 8 | }, 9 | { 10 | "id": "2", 11 | "title": "Star Wars", 12 | "year": 1977, 13 | "description": "Luke Skywalker joins forces with a Jedi Knight, a cocky pilot, a Wookiee and two droids to save the galaxy from the Empire's world-destroying battle station, while also attempting to rescue Princess Leia from the mysterious Darth Vader." 14 | } 15 | ], 16 | "cast": [ 17 | { "id": "1", "name": "Elijah Wood", "movieId": "1" }, 18 | { "id": "2", "name": "Ian McKellen", "movieId": "1" }, 19 | { "id": "3", "name": "Orlando Bloom", "movieId": "1" }, 20 | { "id": "4", "name": "Mark Hamill", "movieId": "2" }, 21 | { "id": "5", "name": "Harrison Ford", "movieId": "2" }, 22 | { "id": "6", "name": "Carrie Fisher", "movieId": "2" } 23 | ], 24 | "reviews": [ 25 | { "id": "1", "text": "It was awesome.", "author": "Sam", "movieId": "1" }, 26 | { "id": "2", "text": "I liked it.", "author": "Ryan", "movieId": "1" }, 27 | { 28 | "id": "3", 29 | "text": "The greatest kids' picture for adults since \"The Wizard of Oz.\"", 30 | "author": "Tom Shales", 31 | "movieId": "2" 32 | }, 33 | { 34 | "id": "4", 35 | "text": "On the basic level of simple entertainment it succeeds as well as any film ever has.", 36 | "author": "Steve Biodrowski", 37 | "movieId": "2" 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /SCRIPT.md: -------------------------------------------------------------------------------- 1 | # Talk 2 | 3 | Hello world of Next 13! 4 | 5 | # Step 6 | 7 | Let's add tailwind: 8 | 9 | ```js 10 | // layout.js 11 | import "tailwindcss/tailwind.css"; 12 | ``` 13 | 14 | No more \_app or \_document! Add classes right to html. 15 | 16 | ``` 17 | 18 | ``` 19 | 20 | # Step 21 | 22 | Right now we've got one page. Let's go ahead and add a page for the `/movies` URL. 23 | 24 | ```jsx 25 | // movies/page.js 26 | export default function Page() { 27 | return

Movies page

; 28 | } 29 | ``` 30 | 31 | # Step 32 | 33 | Now let's add a header with some links. And the root layout is the perfect place for persisent UI like our header. 34 | 35 | ```jsx 36 |
37 | Home 38 | Movies 39 |
40 | 41 |
{children}
42 | ``` 43 | 44 | Links work and the header is persistent! 45 | 46 | Let's turn these into next/links. Now we have clientside transitions. 47 | 48 | # Step 49 | 50 | Ok, it'd be nice to see which link is active. Let's come to our root layout and drop a console.log here whenever this component renders. 51 | 52 | ```jsx 53 | console.log("rendering"); 54 | ``` 55 | 56 | We don't see this running whenever we navigate the page. But we actually also don't see it when we refresh the page. But if we pop over to our terminal, we actually see it running here! 57 | 58 | This is because components in Next 13 are Server Components by default! This is different from Next 12, which pre-rendered React components at build time but then shipped them to the browser to be executed on the client. Server Components actually execute at the time of the request on the server, and ship zero javascript to the client, which is great for making our apps smaller and faster, in addition to some other benefits we'll cover soon. 59 | 60 | But for these links, since we want them to be interactive and change their style as we navigate, we want some good ol' React components right here in the browser. 61 | 62 | So let's create a new component called NavLink. And we can colocate it right here next to our root layout, which is another great feature of Next 13. 63 | 64 | ```jsx 65 | export default function NavLink() { 66 | // 67 | } 68 | ``` 69 | 70 | We'll paste in our link, and lets go ahead and take in our href and children as props. 71 | 72 | ```jsx 73 | import Link from "next/link"; 74 | 75 | export default function NavLink({ href, children }) { 76 | return ( 77 | 78 | {children} 79 | 80 | ); 81 | } 82 | ``` 83 | 84 | And replace them in the layout. So now if we save this, we'll see everything works. 85 | 86 | Now let's come to our NavLink. Right now this is still a server component: if we add a log we don't see it in the browser: 87 | 88 | ```jsx 89 | console.log(href); 90 | ``` 91 | 92 | But we can turn it into a client comopnent with `use client`. Check that out. We see our log in the browser! 93 | 94 | Now that this is a client component, we have some useful new hooks we can use here. The one we want is useSelectedLayoutSegment. 95 | 96 | ```jsx 97 | let selectedSegment = useSelectedLayoutSegment(); 98 | console.log({ href, selectedSegment }); 99 | ``` 100 | 101 | We see as we navigate this shows us which segment the current layout is rendering – basically, what's being rendered into `children` right here in our layout. And we can see when we're on `/movies` the segment is `movies` and when we're on `/` the segment is the empty string. So we should be able to see if a link is active if its href is equal to slash the segment: 102 | 103 | ```jsx 104 | let active = href === `/${selectedSegment}`; 105 | ``` 106 | 107 | And now we can use this in our Link: 108 | 109 | ```jsx 110 | 111 | ``` 112 | 113 | Boom! Active class right here. 114 | 115 | I really love how easy this is, to interleave client components and server components with each other. It's a big part of how Next 13 lets us use the full power of React whenever we need it, while keeping the static parts of our app slim and fast by rendering them on the server. 116 | 117 | ## Step 118 | 119 | Ok, it's time to fetch some data! I have an API running - if we pull it up we can see a list of movies at `http://localhost:3001/movies`. 120 | 121 | So if we come to our /movies page, how might we fetch this data? getServerSideProps? useEffect? 122 | 123 | How about an async function called getMovies? 124 | 125 | ```js 126 | async function getMovies() { 127 | let res = await fetch("http://localhost:3001/movies"); 128 | 129 | return res.json(); 130 | } 131 | ``` 132 | 133 | ```jsx 134 | export default function Page() { 135 | let movies = use(getMovies()); 136 | 137 | return ( 138 |
139 | 144 |
145 | ); 146 | } 147 | ``` 148 | 149 | This is still running on the server! Benefits: fewer client states to deal with - take advantage of request/reseponse cycle. Direct access to server resources. 150 | 151 | > Maybe? Enable slow 3g. Click movies. No response - pretty bad. If we don't want the load-then-render of a traditional server rendered site we can add a loading page. Cool! Instant navigation. 152 | 153 | # Step 154 | 155 | Ok - it's time to make that list-detail view we saw from the beginning. We want this list of movies to be in a sidebar, and the current movie to show up here, something like this: 156 | 157 | ``` 158 |
159 | 164 | 165 |
166 |

Lord of the Rings

167 |
168 |
169 | ``` 170 | 171 | Looks like another layout! Movies in layout, and selected movie renders right here as `children`. 172 | 173 | So let's do that. Let's make this whole thing a layout. And layouts get children. And if we save and reload we see a 404 since we don't have a page here. So let's make one 174 | 175 | ```jsx 176 | export default function Page() { 177 | return

Hi

; 178 | } 179 | ``` 180 | 181 | and now `/movies` is visible again. 182 | 183 | So this page is whats rendered when we're just at /movies - its sort of the index for /movies. And you might use this to have a message, something like "Select a movie!" 184 | 185 | ## Step 186 | 187 | Ok so now we want to make these links go to specific movies using their id – something like `/movies/1` and `/movies/2`. 188 | 189 | So let's make these links go to `/movies/id`. 190 | 191 | ```jsx 192 |
  • 193 | {movie.title} 194 |
  • 195 | ``` 196 | 197 | and now when we click lotr, we go to /movies/1 and we get a 404. 198 | 199 | So let's make this page by using a dynamic segment. The way we do that is by making the folder name with brackets (just like in Next 12, but its called page.js) 200 | 201 | ```jsx 202 | // movies/[id]/page.js 203 | 204 | export default function Page() { 205 | return

    This is a movie

    ; 206 | } 207 | ``` 208 | 209 | And now we're rendering this both at /movies/1 and /movies/2. 210 | 211 | Ok so we want this page to be different for each movie – how do we get which id we're at? 212 | 213 | Dynamic pages get a `params` object as an arg so if we log that out 214 | 215 | ```jsx 216 | export default function Page({ params }) { 217 | console.log(params); 218 | 219 | return

    Movie {params.id}

    ; 220 | } 221 | ``` 222 | 223 | now we can see which id we're on. 224 | 225 | - Nested layouts recap! Good way to build ui. 226 | 227 | ## Step 228 | 229 | To show the movie details we need to actually fetch this movie from the server. We know how to fetch data - copy from layout. 230 | 231 | ```jsx 232 | import { experimental_use as use } from "react"; 233 | 234 | async function getMovie(id) { 235 | let res = await fetch(`http://localhost:3001/movies/${id}`); 236 | 237 | return res.json(); 238 | } 239 | 240 | export default function Page({ params }) { 241 | let movie = use(getMovie(params.id)); 242 | 243 | return

    {movie.title}

    ; 244 | } 245 | ``` 246 | 247 | and all this is still running on the server! 248 | 249 | ## Step 250 | 251 | Let's update our links in our template here to show which movie is selected. We'll create a `movie-link` right here and copy over our nav link. Change to MovieLink, and let's take a look at `selectedSegment` on this one. 252 | 253 | ```jsx 254 | console.log(selectedSegment); 255 | let active = href === `/movies/${selectedSegment}`; 256 | ``` 257 | 258 | And now can customize the active state: 259 | 260 | ``` 261 | className={active ? "text-gray-100" : "hover:text-gray-300"} 262 | ``` 263 | 264 | And it works! 265 | 266 | ## Step 267 | 268 | Ok – let's add some more details from our movie! 269 | 270 | ``` 271 | return ( 272 |
    273 |

    {movie.title}

    274 |

    Year: {movie.year}

    275 |

    {movie.description}

    276 |
    277 | ); 278 | ``` 279 | 280 | And lets see how deep the nesting rabbit hole goes by adding another chunk of UI here to show the cast or the reviews for the movie. 281 | 282 | ```jsx 283 | 287 | ``` 288 | 289 | You know the drill – let's turn this page into a layout, grab the `children` and render them right here. And let's go ahead and make a page for the cast members. I can fetch the cast members for a movie via `http://localhost:3001/movies/1/cast`, so let's fetch some data 290 | 291 | ```js 292 | import { experimental_use as use } from "react"; 293 | 294 | async function getCast(movieId) { 295 | let res = await fetch(`http://localhost:3001/movies/${movieId}/cast`); 296 | 297 | return res.json(); 298 | } 299 | 300 | export default function Page({ params }) { 301 | let members = use(getCast(params.id)); 302 | 303 | return ( 304 |
    305 | 310 |
    311 | ); 312 | } 313 | ``` 314 | 315 | Now, let's turn these into links: 316 | 317 | ```jsx 318 | 319 | Cast 320 | 321 | 322 | Reviews 323 | 324 | ``` 325 | 326 | and build the reviews page. And finally the reviews page. Copy from Cast. 327 | 328 | And look at that. We've got the root layout rendering the movies layout rendering the movies/[id] layout, which renders either the cast or reviews page. And each segment – each layout or page – is loading the data that it needs, right here alongside of it. So this makes for a super easy way to break up our UI, we don't need to go up to the top of the route and keep tweaking a single loading hook like getServerSideProps in Next 12, to make sure it gets data for these nested segments, and then flow that data down. We can co-locate all the data fetching with each segment that needs it. 329 | 330 | Pretty cool! 331 | 332 | ## Step 333 | 334 | Now this whole time we've been building you might be wondering about features from the current version of Next like getStaticProps and ISR – some of next's killer features that make the static parts of our sites super fast. 335 | 336 | Well let's come down to our terminal here, start api, run a build. Instant, movies loaded, API server is silent. Pretty amazing because we wrote this layout code right here with fetch! Right in our component. But because of RSC and suspense, Next can actually render these components at build time and wait for them to unsuspend, before completing the build. So we're getting the equivalent behavior of getStaticProps here, but we're able to just write React components using RSC and the `use` hook. 337 | 338 | Now if we click on a movie, we're going to see we're still fetching this data here. And this is becuase of the dynamic segment. Now in Next 12 we had the `getStaticPaths` API that we could use if we wanted to pregenerate these pages at build time. 339 | 340 | In Next 13 we have a similar api, called `generateStaticParams`. So if we come to the layout for this dynamic segment 341 | 342 | ```js 343 | export async function generateStaticParams() { 344 | let res = await fetch("http://localhost:3001/movies"); 345 | let movies = await res.json(); 346 | 347 | return movies.map((movie) => ({ 348 | id: movie.id, 349 | })); 350 | } 351 | ``` 352 | 353 | Similar to paths but only concerned with the current dynamic segment. So we can fetch our movies and return an array of movie ids that we want to pregenerate. Rebuild, boom. 354 | 355 | Beyond scope of talk but there's way more options for configuring these fetch calls, ways to signal to Next how each one should be cached, whether it should be run at build time or revalidated every 10 seconds or via a webhook using ISR, but regardless very exciting how we basically have just one way to do fetch data, using fetch in a RSC, and then we can mix and match these options to get a fast site or real-time data. 356 | 357 | - Been making nested layouts for years, great way to build apps 358 | --------------------------------------------------------------------------------