Level Up Your Next JS Projects with the “app” Directory.

Tony S. Babu
Walmart Global Tech Blog
8 min readSep 26, 2023

Introduction

Next JS is a popular open-source framework for building web applications with React. It is built on top of React and provides server-side rendering (SSR), static site generation (SSG), and other performance optimisations outside of the box. This popular React framework is constantly evolving to enhance developer productivity and improve the developer experience. With each release, Next JS introduces new features and updates that make building complex web applications even more straightforward. One of the exciting additions in the latest version of Next JS is the new “app” directory.

In this article, we will dive into Next JS’ “app” directory and explore its purpose, benefits, and how it can help developers improve their workflow. We will uncover the key features and demonstrate how to leverage this directory to its full potential.

What is the new ‘app’ directory?

The app directory was created as a follow-up to the Layouts RFC previously published for community feedback. In the previous versions, Next JS supported file based routing using the pages directory. From v13 onwards, while still supporting the pages directory and all of its features, the app directory was introduced, which supports features like defining layouts, error and loading files for individual routes, route groups, React server components, etc.
The app directory can coexist with the current pages directory so that users can incrementally move parts of their application to the new app directory to take advantage of the new features.

1. Defining Routes inside the app directory

Routes can be defined by creating a folder hierarchy inside the app directory.

To create the route /dashboard/settings, the folder structure will be:

Fig 1

The page.tsx file renders the UI for a particular route segment.

Different routes can be organised into groups using Route Groups.
A Route Group allows us to create a folder inside the app directory without affecting the URL structure of the folders nested within it.
A route group can be created by wrapping a folder’s name inside the parenthesis: (folderName)

Fig 2

In Fig. 2, profile and account routes are grouped together inside a user group without their url paths being affected by the (user) route group folder.
The route for profile and account page will remain as /profile and /account.

2. Defining the Layout UI for a route

Layouts are components that are used to create a UI that is shared across multiple pages. These components are named with the convention layout.tsx. A layout for a route can be defined by creating a layout.tsx file at the top level of the route folder.

Fig 3

Fig 3 shows a layout.tsx file that contains UI that is shared across /dashboard/* route pages.

One thing to note here is that the layout.tsx component inside the dashboard folder, preserves its state while the route changes between /dashboard/settings and /dashboard/analytics.

A root layout is mandatory for the app directory.
It can be created by adding a layout.tsx file at the top level of the app/ directory.

Fig 4

The root layout can be used to manipulate the HTML returned by the server to the entire app at once.

/* app/layout.tsx */

export function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}

The root layout does not render again upon navigation, so any data or state in the layout will persist throughout the lifecycle of the application.

Layouts are nested by default. For instance, if we navigate to the /dashboard/* route, the root layout will be rendered, and the layout and UI defined for the /dashboard/* route will be rendered as children of the root layout.

Fig 5

3. Defining the loading UI for a page

We can define a default loading skeleton for a route by adding the file loading.tsx inside the route folder.

For example, /dashboard/settings/loading.tsx will contain the loading skeleton, which shows up when navigating to the /dashboard/settings route.

Fig 6

The loading.tsx file should export a React component.

Behind the scenes, defining a loading.tsx file for a route will wrap the route’s page or layout with a Suspense boundary, whose fallback will be the React component returned by loading.tsx

/* app/layout.tsx */
export function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
/* app/dashboard/loading.tsx */
export default function Loading() {
return <YourSkeleton />;
}

/* Output of /dashboard */
<html lang="en">
<body><Suspense fallback={<Loading />}>{children}</Suspense></body>
</html>

4. Defining the Error UI for a page

An error UI for a route can be implemented by creating an error.tsx at the top level of the route’s folder.

Fig 7
/* app/dashboard/settings/error.tsx */
export default function Error({
error,
reset,
}: {
error: Error
reset: () => void
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button
onClick={
// Attempt to recover by trying to re-render the segment
() => reset()
}
>
Try again
</button>
</div>
)
}
/* app/dashboard/settings/page.tsx */
export default function SettingsPage() {
return <SettingsPageContents />;
}

/* Output of /dashboard/settings */
<html lang="en">
<body><ErrorBoundary fallback={<Error />}><SettingsPage /></ErrorBoundary></body>
</html>

The component defined in error.tsx will be shown as a fallback if any error is thrown from within the route or its subtree. Through this, we can isolate the errors to that part of the subtree, while the remaining application remains functional. Layouts above the error boundary will remain interactive, and the state is preserved.

Note that error.tsx will not catch errors within a sibling layout.
Errors inside layouts are only caught by ErrorBoundaries created one level above.

eg: /dashboard/error.tsx wont catch errors inside /dashboard/layout.tsx,
/dashboard/error.tsx will catch errors inside /dashboard/settings/layout.tsx.

To catch errors inside the root layout, we should create a file named global-error.tsx at the top level of the app directory.
The error boundary created by the global-error.tsx file wraps the entire application.
And the fallback component replaces the root layout in the event of an error.
Similar to the root layout, global-error.tsx should define its own html and body tags.

/* app/dashboard/settings/global-error.tsx */

export default function GlobalError({
error,
reset,
}: {
error: Error
reset: () => void
}) {
return (
<html>
<body>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</body>
</html>
)
}

5. React Server Components

React Server Components are React components designed to run on the server. Components in the app directory are React Server Components by default. React Server Components provide benefits such as leveraging the server infrastructure and keeping large dependencies server-side, leading to better performance and reduced client-side bundle size.

In React server components we cannot use.

  • React hooks
  • React Context
  • Browser only APIs

Data fetching in Server Components can be done by creating the component as an async function and using await to fetch data inside the component.

import React, { Suspense } from "react";
export async function getProducts() {
const res = await fetch(`http://localhost:3000/api/categories`);
const products = await res.json();
return products;
}
export default async function Dashboard() {
const products = await getProducts();
const brands = await getBrands();
return (
<div>
<h2>Products</h2>
{products ? (
<ul>
{products.map((product) => (
<li>{product.name}</li>
))}
</ul>
) : null}
</div>
);
}

With Server components, we are able to instantly render parts of the page that do not need external data fetching while showing a loading state for parts of the page that are fetching data. The user does not have to wait for the entire page to load before starting to interact with it.

import React, { Suspense } from "react";
export async function getProducts() {
const res = await fetch(`http://localhost:3000/api/categories`);
const products = await res.json();
return products;
}
export async function getBrands() {
const res = await fetch(`http://localhost:3000/api/brands`);
const brands = await res.json();
return brands;
}

export default async function Dashboard() {
const products = await getProducts();
const brands = await getBrands();
return (
<div>
<h2>Products</h2>
{products ? (
<ul>
{products.map((product) => (
<li>{product.name}</li>
))}
</ul>
) : null}
<Suspense fallback={<div>Fetching brands...</div>}>
<ProductBrands brandsPromise={brands} />
</Suspense>
;
</div>
);
}

async function ProductBrands({ brandsPromise }) {
const brands = await brandsPromise;

return (
<ul>
{brands.map((brand) => (
<li>{brand.name}</li>
))}
</ul>
);
}

In the above example, when we navigate to the /dashboard route, the fetch calls for getProducts and getBrands are triggered in parallel.

Once getProducts is resolved, the list of products is rendered on the server and sent to the browser, and the page becomes interactive. A fallback UI is displayed instead of <ProductBrands />. Once getBrands is resolved, the <Productbrands /> is rendered on the server and streamed to the browser.

Summary

  • The app directory introduces many new features, including a new way of defining routes, separate loading and error UIs, layouts for individual routes, and support for react server components.
  • Routes are created by creating folders inside the app directory.
  • Different routes can be organised into groups using Route Groups.
  • A route group can be created by wrapping a folder’s name in parenthesis: (folderName)
  • Layouts for individual routes can be created by adding a file named layout.tsx at the top level of the routes folder.
  • Layouts are nested by default, the layout for an individual route will be rendered inside the root layout.
  • Root Layout should return HTML and body tags.
  • Root Layout does not render again upon navigation, and its state is persisted throughout the lifecycle of the application.
  • A loading UI for individual routes can be created by adding a file named loading.tsx at the top level of the routes folder.
  • Defining a loading UI for a route will wrap the route’s layout or page with a suspense boundary.
  • Error UI for individual routes can be created by adding a file named error.tsx at the top level of the routes folder.
  • Defining an Error UI for a route will wrap the routes page with an error boundary.
  • Creating a global-error.tsx file will catch the errors in the root layout.
  • React Server Components are React components designed to run on the server.
  • React Server Components allow data fetching in the component body using async await.

Conclusion

In this article, we discussed the features in the new “app” directory introduced in Next JS v13. All these features are stable from v13.4 onward and can be incrementally adopted in an existing application alongside the /pages directory.

--

--