31 May 2023 (updated: 31 May 2023)
Chapters
Next.js 13 presented many exciting updates and solutions in the beta version. One of them is the /App directory, a new version of a file-system-based router. We analyze these changes and compare how they will impact the way applications are created in the future.
When creating applications using the Next.js framework, the central point of each of them is the /pages folder. Every newly created file in this folder (with the appropriate extension) is a new route, which in short means that the /pages folder is actually nothing more than a file-system-based router. We can easily create nested routes by using nested folders or dynamic routes using [ ] in the file name.
Next.js version 13 introduced the /app directory. This is a new version of a file-system-based router where folder names are used to define the appropriate routes. Each folder defined in the /app directory and containing a component page inside is a separate route in our application.
To make it easier for us to transition from /pages to /app, Next.js developers made sure that both approaches are compatible with each other. This means that we can use both the /app and /pages folders as long as there are no conflicts between them, for example, if we have a route named users defined in both the pages and app folders.
But why all this? Why switch from /pages to /app when, in reality, nothing changes except for a different way of defining routes? Well, not quite! The transition to the new way of creating routes would not make sense if it weren't for the fact that Next.js offers us a huge amount of new features and solutions! In the following sections, I will introduce and discuss some of the more important ones.
Let's say we want two pages in our application to have certain common parts. In the case of using the /pages folder, the optimal way would be to create a component in which we place the repeating elements (e.g. a side menu), and then wrap both pages with it.
Both pages share components placed in RepositoryLayout. However, this is not an ideal solution. Firstly, we have to wrap both pages separately in our wrapper, and secondly, we will have unnecessary re-renders when switching between these pages.
After opening the dev tools and turning on the Highlight updates when the components render option is available in the Profiler tab, we can see that when we switch from one page to another, our common components placed in RepositoryLayout are also re-rendered.
This is a behavior that we would obviously want to avoid in situations where a certain portion of the UI is shared between a group of routes.
Next 13 introduced a special component called layout that significantly simplifies the process of sharing UI. We can place layout files at any level of our /app folder.
Let's rewrite the above example using features available since Next 13. Besides changing the paths, we can notice that there is significantly less code in the individual components corresponding to our subpages. This is because we no longer need to wrap them in the RepositoryLayout wrapper.
All the code that was previously in our wrapper is now placed in the /app/repositories/layout.tsx file, which is nothing more but a wrapper for the page files in the same segment. This allows both repository subpages to share the appropriate UI.
It should be noted that the mentioned layout will only be shared for pages in the /app/repositories segment. If we create a new route, for example, in the /app/users directory, the pages in that folder will not share the layout located in the /app/repositories folder.
As we can see, the shared UI does not re-render when switching between pages.
An important point to note is that layout files are essentially optional. I say essentially because there must be one layout file located directly in the /app directory. In this file, we have the ability to edit the default HTML returned by the server, for example. The main layout reflects the _app.tsx and _document.tsx files known from the /pages directory.
Both page and layout are file names reserved in the /app folder. However, that's not all. Next 13 has also introduced a special file called loading that allows us to wrap the page file in a Suspense boundary.
To better understand the benefits we can achieve with the loading file, let's talk about streaming. Until now, whenever we wanted to take advantage of SSR, we used functions like getServerSideProps. Inside this function, we retrieved the necessary data from our API and then Next rendered the page on the server side, sent its non-interactive version to the client, and used hydration to enrich it with, for example, listeners. However, this approach has a downside. We have to wait for hydration until all data fetching on the server side is completed, which can significantly prolong the First Contentful Paint (FCP), for example.
To prevent this, we can use the features provided by Next 13 and break our page into smaller parts, for example, by fetching and rendering important repository information in the layout file and fetching and streaming less important information as quickly as possible. With the loading file, we can display a placeholder UI, in the form of a skeleton for example, until the main content in the page file is fetched and rendered.
Let's add the layout.tsx file and retrieve important repository information in it. The less important information will be fetched on the page.tsx file. Let's also create the loading.tsx file and export a simple skeleton in it.
As you can see, thanks to streaming, we don't have to wait for all the data to be fetched. The main content is fetched and displayed to the user as quickly as possible, and the parts of the page that would significantly prolong the time it takes to deliver the page to the user have been wrapped in Suspense.
Instead of them, we render a placeholder UI (our skeleton) placed in the loading file, and as soon as the data is fetched, we stream it and replace the skeleton with our target content. Since streaming is server-side rendered, we don't have to worry about SEO issues either.
In the code above, you could notice one interesting thing that we couldn't do before Next 13. I'm talking, of course, about awaiting promises directly in the component, as in the case of the layout or page file above. We can do this because usually, every component located directly in the /app folder is a Server Component, including files such as layout or page. Next 13 introduced support for Client/Server components. But what exactly are these Server Components?
A Server Component is a component that only renders on the server side. This allows, among other things, to significantly reduce the size of the JavaScript bundle sent to the client.
Client Components, on the other hand, are just React components that we already know, which are pre-rendered on the server side and then hydrated on the client side. They practically do not differ from what we know, for example, from Next version 12 and the /pages folder. To create a Client Component, you need to use the 'use client' directive at the beginning of the file.
The important question remains: when should we use Server Components and when should we use Client Components?
We already know that with Server Components, we can fetch data from an API, but what about user interactions or using hooks such as useState or useContext? Since Server Components only execute on the server side, we cannot use React mechanisms like state handling in them. To make it easier and clearer for us to understand which components are useful for which purposes, the Next developers have prepared a special table available here, which helps to easily visualize the use of components.
Whenever we have the opportunity to use Server-side components, we should do so because reducing client-side components can significantly reduce the size of the bundle sent to the user.
Speaking of data fetching, you may have also noticed that we haven't used any of the functions we know for fetching data, such as getServerSideProps or getStaticProps, in our code so far. That's right, we haven't used them and we won't be using them!
The necessary functions that allowed us to fetch data and prerender our page on the server side are no longer needed and not supported in the /app directory. Instead of fetching data using getServerSideProps, for example, we can directly fetch data in Server Components using the enhanced fetch API for example.
Next 13 has enriched the fetch API with several interesting functionalities. First of all, every call to the fetch function is cached by default (parameter cache: force-cache), which among other things means that pages that make API queries will be statically generated during the build.
This means that based on the call to the fetch function in a page component, Next determines whether the page will be generated statically, as it would with the getStaticProps function in the /pages folder. On the other hand, if we want the page to be dynamically generated, as with the getServerSideProps function, we can pass the cache: no-store parameter to the fetch function, which will cause the page to be dynamically generated at request time.
We must remember that there are also several special functions that, when called, will cause Next to change the way a page is generated from static to dynamic, even though the cache parameter in fetch function calls is set to force-cache. These are the so-called Dynamic functions: cookies, headers, and useSearchParams. The values returned by them cannot be known in advance during build time.
To sum up, if we want a page to be generated statically (getStaticProps), there can be no calls to fetch functions with the cache: no-store parameter or dynamic function calls listed above in that segment. Otherwise, the page rendering will be changed to dynamic (getServerSideProps).
What about dynamic routes and getStaticPaths? Next 13 has also made changes here. Instead of the getStaticPaths function, we now have the generateStaticParams function. It works similarly to the previous function with some minor improvements that relate, among other things, to more economical data fetching.
If we use the previously mentioned Fetch API, repeated requests in files such as layout, page, and generateStaticParams function will be automatically deduplicated, which means that instead of three requests, only one will be executed. This will have a positive impact on the time required to build the application.
Resignation from the data-fetching function, support for React Server-Components, changes in the way routes are defined, and new components such as layout, loading, and more - these are just some of the changes presented in Next 13. I highly encourage reading the excellent Next documentation and thoroughly familiarizing oneself with the new features related to the /app directory. It's worth paying attention to the error file, which allows us to wrap components in a specific segment in an Error Boundary.
As we can see, the /app directory significantly changes the style of building applications. Is it for the better? It's hard to give a definitive answer to that question.
However, proper use of the /app directory will allow us to optimize and improve the performance of our applications. In the case of the /pages folder and the previous approach to application development, we often encountered performance issues when, for example, our subpage was large and the getServerSideProps function contained requests that significantly prolonged the time required to deliver the page to the user.
Thanks to streaming, we can easily get rid of this problem. The use of Server-Components will also allow us to reduce bundle size, which will have a positive impact on the page's performance. Another positive aspect of the /app directory is that we can keep components closely related to specific routes directly in the route folder, which was not possible with the /pages folder. In my opinion, this will have a positive impact on the organization of the codebase in projects.
However, you should remember that all the features mentioned before are currently in beta and are not suitable for production applications.