← Back to listing

Navigating the Future Within the Next.js App Router

DATE

AUTHOR

Joaquin Montes

 / 

Agu Seguí

CATEGORY

Development

READ TIME

7 minutes

VIEWS

...
With every new paradigm shift, new patterns emerge. Dive into how we leverage the powerful Next.js App Router and avoid common mistakes.

Navigating the Future Within the Next.js App Router

author
Joaquin Montes / Agu Seguí
Hero Next.js App Router

It has been a year since Guillermo Rauch first announced the App Router paradigm at Next.js Conf in SF. Despite the time that has passed, many web developers still find the App Router paradigm unfamiliar and challenging. I was in the same boat for a while.

This post will discuss the challenges we faced when migrating to the App Router and introduce some new core concepts it brings to the Next.js development process. Our goal is to save you some headaches when experimenting with this not-so-new paradigm and, most importantly, encourage you to dive into what the Vercel team has brought to the game.

This article isn't going to cover all aspects of the App Router. If you need a detailed guide, the Next.js documentation, which is improving steadily, is your go-to, alongside the awesome Learn experience that's been totally updated. In this piece, we'll discuss some challenges we've faced and the solutions we've come up with. This could be handy for you. We'll also highlight some important parts of the official docs that interest you.

Clarifying Misconceptions

To avoid confusion, let's clear up a couple of misconceptions about the relationship between the App Router, Next.js features, and React.

Next.js 13 is not the App Router

While the App Router was first introduced with the release of Next.js 13, it's important to note that they are not coupled together. Next.js 13 (and subsequent ones) has its own updates, and you can use these features alongside the old, familiar Pages Router.

What do React Server Components actually do?

To make decisions about migrating to the new Next.js paradigm, we need to know that React Server Components (RSC) are integral to the new App Router paradigm. Server Components run server side and send just the results to the client, bypassing hydration and not adding their own weight (in terms of JS) to the client bundle. This can significantly reduce the overall bundle size of your application, especially as you add more and more components.

Leveraging RSC Benefits

Many common tasks involving fetching data and performing server-side calculations, previously done in getStaticProps or getServerSideProps, can now be declared within the component itself. This simplifies the component's logic and provides more granular control over what should be static, revalidated, or blocked until fresh data is ready.

What about using hooks and managing state? Then, you’d use a Client Component, of course! But it’s important to structure your components so that you can fully leverage RSC benefits and sprinkle in the interactivity without making your whole app “use client". To that end, it's advisable to avoid using client components near the root level of your application (except for context and analytics providers) and your page.js files. This doesn't mean client components are bad; they are still suitable for handling state and user interactivity, but they’ll contribute to the overall JS bundle size you send to the client. This is key to thinking about your code architecture. Otherwise, you will have your entire application without leveraging RSC at all.

Don’t get lost across the component nesting

How can I know when I’m working on the client or server side? If you are working on a very nested component, until you get used to the new mindset, you could have doubts about which of the two environments this component lives. There isn’t a clear and definitive way for all cases, but you can use a couple of alternatives to your advantage.

  1. The first and fastest one would be trying the old handy console.log. Is the result printed on the browser’s console? Then you’re client side. Otherwise, in development, if it’s printed on your terminal (where you ran your next.js app), then you’re on a server component.

  2. Another way is to use hooks. React, and by extension Next.js, won’t let you use them on a server component and will throw an error on your browser.

An important point to make, too, is that if you have some code you want to ensure only runs on the server and doesn’t pollute the client bundle, you can use server-only, as explained here.

Seamless Pivot Between Both Environments

Mastering both aspects within our team was key to adapting to this new paradigm. Let's explore some common use cases we faced along the journey:

How to nest RSC in Client Components

Sometimes, you’ll have some code that could work fine as a Server Component, but it is inside a client component. Since we can’t import server components into client components, we need to create a ‘slot” by passing it as a prop using the React children prop:

1// DON'T
2'use client'
3// This will be imported as a client component
4import ServerComponent from '...'
5
6const ClientComponent = () => {
7 return <ServerComponent />
8}
1// DO
2'use client'
3const ClientComponent = ({ children }) => {
4 useEffect(() => {
5 // some client logic
6 }, [])
7
8 return (
9 <div>
10 {children}
11 </div>
12 );
13}
1// DO
2import ClientComponent from "./ClientComponent";
3import ServerComponent from "./ServerComponent";
4
5// Pages are Server Components by default
6export default function Page() {
7 return (
8 <ClientComponent>
9 <ServerComponent />
10 </ClientComponent>
11 );
12}

To dive deep further into this, we encourage you to take a look at the App Router Migration Guide from Next.js

Moving interactive logic down the tree

Something common on the old pages router was wrapping the main content under Providers and running some global hooks, analytics, etc., on the _app.tsx file. Now, we keep our layout.tsx file as an RSC and import client components from providers.tsx. This way, we ensure not to send all the component JavaScript of the layout to the client.

Before (Pages Router)

1// ~/pages/_app.tsx
2import '~/css/global.scss'
3// ...imports
4
5/* CUSTOM APP */
6
7const App = ({ Component, pageProps, ...rest }: AppProps) => {
8 useGoogleAnalytics()
9
10 const getLayout: GetLayoutFn =
11 (Component as any).getLayout ||
12 (({ Component, pageProps }) => <Component {...pageProps} />)
13
14 return (
15 <>
16 <Head>
17 <AppScripts />
18 </Head>
19 <SegmentTrackerProvider>
20 <SegmentPageChange />
21 {getLayout({ Component, pageProps, ...rest })}
22 </SegmentTrackerProvider>
23 </>
24 )
25}
26
27const SegmentPageChange = () => {
28 const { analytics } = useSegment()
29 const { asPath } = useRouter()
30
31 React.useEffect(() => {
32 analytics?.page()
33 }, [analytics, asPath])
34
35 return null
36}
37
38/* APP HOOKS */
39
40const useGoogleAnalytics = () => {
41 React.useEffect(() => {
42 // Some client logic, e.g.: using event listeners or router events
43 }, [])
44}
45
46/* EXPORT */
47
48export default App

After (App Router)

1// ~/app/layout.tsx
2import '~/css/global.scss'
3
4// ...imports
5import { AppProviders } from './providers'
6
7/* CUSTOM APP */
8export const metadata = { title: 'Meilisearch' }
9
10export default function RootLayout({
11 children
12}: {
13 children: React.ReactNode
14}) {
15 return (
16 <html lang="en">
17 <body>
18 <AppProviders>
19 {children}
20 </AppProviders>
21 </body>
22 </html>
23 )
24}
1// ~/app/providers.tsx
2'use client'
3//...imports
4
5export const AppProviders = ({ children }: { children: React.ReactNode }) => {
6 useGoogleAnalytics()
7 const { analytics } = useSegment()
8 const pathname = usePathname()
9
10 React.useEffect(() => {
11 analytics?.page()
12 }, [pathname, analytics])
13
14 return (
15 <>
16 <Toaster visibleToasts={1} />
17 <SegmentTrackerProvider>
18 <ThemeProvider disableTransitionOnChange defaultTheme="dark">
19 {children}
20 </ThemeProvider>
21 </SegmentTrackerProvider>
22 </>
23 )
24}

You can define multiple "use client" entry points in your React Components Tree. This allows you to split your application into multiple client bundles (or branches). However, "use client" doesn't need to be defined in every component that needs to be interactive. Once you define the boundary, all child components and modules imported into it are considered part of the client bundle. — Next.js Documentation

Real-World Use Cases

Alright, let's take a look at a few examples within basement.studio where the App Router really made a difference because of its capabilities:

Meilisearch documentation: This documentation is loaded from mdx remote files that should be transformed into HTML. App Router, alongside next-mdx-remote, allowed us to do this expensive transformation in the server and send the client just the HTML, without the need to import a lot of JavaScript code like the one react-markdown uses on the client.

Another big win on this project was handling the big load of syntax highlighting calculations on the server. Syntax highlighting libraries can be very heavy to bundle, so keeping them on the server is better. This way we saved 95% of the bundle size.

BaseHub: In this case, the entire web application used the App Router from scratch, and alongside caching, edge runtime, and other features, it enables a blazingly fast experience where every navigation has some initial data that syncs to the server asynchronously.

Understanding The App Router Caching

First, it's important to understand that Next.js will try to cache as much as possible to boost performance and minimize costs. As a result, routes are statically rendered, and data requests are cached unless you decide to opt out through additional configurations. In the RSC environment, when a build is executed, the default setting pre-renders pages (and route handlers). Now, let's explore how to deal with fresh data.

A nice thing we’ve found is that caching features turned out to be more granular when you opt-in to dynamic rendering. We were used to managing multiple fetch functions that passed through a prop with shared settings.

Now, the "one getStaticProps to rule them all" approach was superseded by a new paradigm that allowed us to handle data fetching at the component level. This gave us the ability, for instance, to establish unique revalidation for each component separately.

Caching on Next.js is an evolving paradigm right now. It appears to be moving towards a more defined structure. However, there are ongoing discussions about the use of polyfills and custom utilities that the framework extends from the conventional Web APIs. Finding a good balance between exposing new features and abstractions while sticking to Web Standards is not as easy as it sounds. However, having the option to choose is something we see as a promising sign. As we were writing this, this great in-depth overview was released with the current state of the App router cache, and we highly recommend a dive deep into elements like revalidateTag.

Conclusions

Transitioning from Pages to App Router might be a bit of a hurdle, as it requires adapting to new paradigms that often accompany such progressive leaps. That said, it does present the potential for significantly reducing the client bundle and offering precise control over data fetching.

Since you can gradually introduce some of its key features, we're now incorporating them into nearly all our new projects while also migrating some existing ones that could benefit from it. Before fully committing to the App Router, we'd really like to see a robust solution for managing page transitions - something we're currently handling in the Pages Router environment. We're excited to see what the future holds this year, so keep an eye out - we'll definitely share more insights as we continue our learning journey.

Further resources

↖ Back to listing
CATEGORYDevelopment
DATE
AUTHORMatias Perez / Matias Gonzalez

Creating Daylight | The Devex

Discover how we enhanced our development process for the Daylight project, from debugging tips to performance boosts maintaining a clean codebase. Meet you down below!

categoryDevelopment
authorMatias Perez / Matias Gonzalez
date

DATE

AUTHOR

Matias Perez

 / 

Matias Gonzalez

CATEGORY

Development

READ TIME

7 minutes

VIEWS

...
CATEGORYDevelopment
DATE
AUTHORMatias Gonzalez / Matias Perez

Creating Daylight | The Shadows

Ever wondered how we created those lifelike shadows on Daylight's website? Here we break down the secrets of our shadow rendering process.

categoryDevelopment
authorMatias Gonzalez / Matias Perez
date

DATE

AUTHOR

Matias Gonzalez

 / 

Matias Perez

CATEGORY

Development

READ TIME

8 minutes

VIEWS

...