Understanding NextJS App Router: A Comparison with React Router

Selim
19 min readJun 23, 2024

--

Hello everyone, In this article, I will explain what the app router in NextJS is, how it facilitates our projects, and how we can use it. Let’s dive into these questions and discover the benefits and usage of the app router in Next.js.

In the first part, we will perform a simple routing setup from scratch using the React Router library version 5.3.4. This is intended for readers who may not know what routing is or how it is done in React. I don’t prefer the 6th version, but there aren’t tremendous differences between the 5th and 6th versions, so understanding the 5th version will suffice. This will also allow us to clearly see the differences between the basic routing in React and the App Router in NextJS. In the next part, we will discuss what the App Router is and what benefits it offers. Then, we will build a routing system from scratch using the App Router and dive deeper into its workings through that system.

Part 1 — React Router:

Let’s start with “What is Routing?”. There are two types of Routing: Network Routing, which is not our topic here, and Application Routing, which is what we will focus on.

Imagine an e-commerce website. You open the site, and the homepage appears with login/register buttons at the top and products displayed all around. Let’s say you like a product and click on it, or you want to log in and click the login button. The site changes your browser’s URL to “/login” and shows you the login page, right? When you click the login button, you expect it to navigate you to the “/login” route, and as a result, the URL in your browser becomes “/login”. At this point, we render the component that we want to display for this URL, which, in this case, is the Login component. This process is called routing. In simple terms, routing can be understood as the process of determining which page or component to render based on the URL — if the URL is “/login”, render the login page; if it’s “/home”, render the home page, and so on.

I start by setting up a React project using Vite and then remove the unnecessary parts. Next, I install version 5.3.4 of React Router by running the command npm i react-router-dom@5.3.4. Now, I have a completely empty React application, and I'm ready to start with routing.

The routing system is usually set up in App.jsx, so that's what I did. The purpose of this is to have a routing system in place as soon as we render App.jsx into our HTML using Main.jsx. While it could technically be done in lower-level components, this would typically be impractical and illogical.

Inside App.jsx, we start by importing the BrowserRouter tag from React Router and use it to wrap our routing setup. Within this, we use a Switch structure, which is similar to a switch/case statement in programming. Inside the Switch, we use Route tags to specify which component to render for each URL. For example, we can set up the Route so that the Home component is rendered when the URL is "/" (i.e., "http://localhost:3000/").

React Router

Exact:

However, there’s an important point to understand here. As seen in the first Route tag, there is a keyword called exact. We use this keyword when we want the URL to match exactly with the specified path. I know it might sound confusing, so let me explain with an example.

Let’s say we remove the exact keyword from the first Route and set our URL to "/about". What would we expect? We would expect the second Route to match and render the About component, right? However, the Home component will actually be rendered. Why does this happen?

React Router checks the current URL and starts from the topmost Route within the Switch structure, then goes downwards to find a match. If we don't use the exact keyword on "/" in the first Route, it essentially means "render the Home component for any URL that contains /". Therefore, because "/about" contains "/", the Home component will be rendered since it matches the topmost route.

To ensure that the About component renders when the URL is "/about", we need to move the Route for "/about" above the Route for "/". Alternatively, we can add the exact keyword to the Route for "/" to specify that we want to render the Home component only when the URL is exactly "/".

In summary, using exact in React Router specifies that the path must match the URL exactly, without considering partial matches.

Navigation (Link):

So, how do we change the URL? We don’t manually type “/about” or “/profile” into the browser’s address bar. There are two ways to achieve this. The first method, as you can see in the application, involves using the Link components inside the navigation (Nav). These components come from React Router and allow us to change the URL using the to attribute. This triggers our routers and renders the desired component accordingly.

You can achieve the same using an <a> tag from HTML, but there's a crucial difference. If you use an <a> tag instead of the Link component provided by React Router, yes, you can change the URL and trigger the routers to render the desired component. However, the significant point here is that when you change the URL using an <a> tag, you'll notice the page refreshes. On the other hand, when you navigate using the Link component, the page won't refresh. You can observe this non-refresh behavior in the top left corner of the video below. I changed the URL to "/about" using an <a> tag, and whenever I click on "About" to go to "/about", the page refreshes. This doesn't happen with the Link components, which is what we want.

The two biggest differences between the Link component and the <a> tag are as follows: First, the <a> tag refreshes the page and sends a new request to the server, whereas Link uses the History API to facilitate transitions between components without making a new request. This efficiency can lead to significant resource savings.

The second benefit, which is closely related to the first, is that when using the <a> tag, the page refresh and server request cause the loss of current state (such as form input values entered by the user). However, with Link, this state is preserved.

In essence, when dealing with routing, it’s advisable to use Link for these reasons.

useHistory :

Now, you might wonder, “Can I only use Link when redirecting users to other routes? For example, after a user fills out a login form, can't I redirect them to "/" if they successfully log in without clicking any Link ?" As you've rightly guessed, of course you can do that. However, there are slight differences between React Router v5 and v6. Since I'm using v5 in this article, I'll use the useHistory hook. Readers using v6 can refer to the <Redirect /> component in the React Router documentation.

The useHistory hook is used as follows: In the simple login function depicted in the image below, I've written a basic login function. First, to use the useHistory hook, we declare const history = useHistory() on line 6. The useHistory hook returns an instance of history, allowing us to perform operations using history. When the button is clicked, it sends a POST request to "http://localhost:3000/login". If the backend returns true, we redirect the user to the home page by using history.push("/").

useHistory

Dynamic Routing :

Lastly, let’s talk about the third router. Here, there’s something different with “items/:id”. The colon “:” at the beginning of “id” indicates that this word is a placeholder, meaning something will come here (in this case, an item ID), and you can capture that incoming thing with the word “id”. Looking back at our navbar, our last three links are “items/1”, “items/2”, and “items/3”, so in these URLs, :id has been replaced with 1, 2, and 3.

In this “items/:id” route, we said that if this route is called, the ItemDetails component should be rendered. But I want the content of this component to be dynamic. That is, according to the ID of the item in the URL, the content of ItemDetails should change. For example, I will print the ID of the item on the screen. To achieve this, we will use the useParams hook from React Router. This hook is used as shown in the image to capture a value from the URL. As we mentioned earlier, we will use ":id" to capture this value, and here by saying const {id}, we capture the ID from the URL and render it inside a "p" tag.

useParams

Why would we need something like this? In fact, we have a great need for it, and we use it every day. For example, imagine you’re browsing an e-commerce site and click on a product. The URL changes, and the corresponding component renders. If we were to create a separate component for each product while managing millions of products, it would not only be incredibly difficult, even impossible, but also highly impractical.

That’s why we create a single component called “Product” and use useParams to capture the ID of the product from the URL we clicked. This way, instead of creating separate components like <ItemOne />, <ItemTwo />, <ItemThree /> for routes like "items/1", "items/2", "items/3", we dynamically render the <Item /> component using the ID from the URL with useParams. This approach allows us to solve the problem dynamically with just one component, handling potentially millions of items.

While I haven’t covered React Router library in its entirety, I’ve explained everything necessary to understand its general concept. Now, we can move on to the main topic.

Part 2 — App Router:

The purpose of using NextJS’s App Router is the same: to handle routing within the application. However, NextJS has implemented this functionality uniquely and seamlessly integrated it into the framework itself, rather than offering it as a separate library.

I’m starting a NextJS project from scratch to examine this. I’m using the command npx create-next-app@latestand answering the questions as shown below. This project is for testing purposes, so I opted not to include ESLint and TypeScript, which I would typically install for a regular NextJS project setup.

I also chose not to create an “src” folder because I don’t need additional folders like components or services for this demonstration. Our main focus here is on the App file.

As you can see, here’s a NextJS application in front of me. I’ve emptied the contents of the page.js file inside App and returned an h1 element. When I open the application, this will appear because this page.js corresponds to my “/” URL in the App Router. You’ll understand this in more detail shortly.

Now, I want you to look into the app folder. What you’re looking at is our router. Yes, you heard that right. Each folder inside the app folder corresponds to a Route within our switch structure. This means that to create a new route, all we need to do is create a new folder under the app folder, such as a folder named “home”.

When we create the “home” folder, we now have a “/home” route. However, for this route to render something on the screen, we create a file named page.jsx inside the home folder. This follows a rule, so don’t create a file with a different name; the App Router will look for a file named “page” to render output for the “/home” route.

Creating a route is quite easy, isn’t it?

Nextjs App Router Tree

Got it. Let me explain again with another example. When we create a folder named “login” under the app directory, we automatically establish a “/login” route. So, whenever we enter “/login” in the URL, what gets rendered on the screen is determined by the contents of page.jsx inside the “login” folder.

Layout:

Alright, what about layouts? If you have a piece of code that you’ll use repeatedly under every route within a specific route, you can place that piece of code in a layout. This way, it can affect all routes under that specific route. Each layout file accepts children as a prop, which represents the component underneath it. The layout renders this component on the screen and allows us to add content above or below children. If this sounds confusing, let me explain with an example from below.

As you can see, under my app directory, there are folders named home, about, and login, each containing a page.jsx file. This means I have routes for “/login”, “/home”, and “/about”.

Each route can have its own page file as well as its own layout file, but generally, we don’t add a layout to every route unless it’s necessary. In this example, under the app directory (“/”), I’ve added “LAYOUT” above children in layout.jsx. Let me explain what happens in this case. Unless stated otherwise, we'll see the "LAYOUT" text across all routes under the app directory. For instance, in the page file under the home route, I've written "Home" without adding anything like "LAYOUT", but when navigating to "/home", "LAYOUT" appears above "Home". Why? Because in the top-level layout file, which is in the app directory, I've returned the text "LAYOUT" above children in its render.

App Router Layout Example

However, now we have a problem: I want “LAYOUT” to appear across all routes, but specifically for the home route and its children routes (like profile), I want “HOME LAYOUT” to be displayed as well. If I add “HOME LAYOUT” directly to the layout.jsx file under the app directory, it will appear not only for the home route and its children but also across all routes. I only want this for the home and its children routes. Let me explain this with an example.

As you can see, I’ve created a profile route under the home route. Inside the home folder, I’ve created a layout.jsxfile and added “HOME LAYOUT” above children.

Profile Layout

As you can see in the screenshot above, when navigating to “/home/profile” route, first the text “LAYOUT” from the top-level layout in the app route hierarchy was displayed. Then, the layout under the home route displayed “HOME LAYOUT”, and finally, the page.jsx under the profile route returned the text “Profile” to be rendered. As observed, I see three different texts on the screen.

Where and how can we use this feature? What purpose does it serve? To put it simply, is there a better place than the top-level layout in the hierarchy to place a navbar? Once I write my navbar above children in the top-level layout, I can see this navbar across all routes without needing to write it in each page.jsx. The same can be said for a footer as well.

Route Groups :

But this might create another issue. While it’s nice to write the navbar once and avoid rewriting it, you might not want a navbar or footer on the login page. So, what do you do?

There are many ways to handle this, but I’ll show you what I think is the most appropriate, and in doing so, we’ll also discover a new feature. We can solve this problem using what we call “Route groups”.

Our goal is to prevent the navbar or footer from appearing on the login page, while ensuring they appear on all other pages. We don’t want to add the navbar and footer to every page.jsx file, so we’ll use layouts to achieve this.

Here’s how we solved the problem as seen in the above image. What did we do here? Firstly, we see things like “(auth)” and “(others)”. These are called route groups. These parentheses allow us to group routes within routing without creating a route like “/(auth)” — we still have “/login” but within a group.

Additionally, we took what we wrote in the top-level layout within the “app” folder hierarchy and placed it into the layout we created inside the “login” folder. This way, while we still obtain the same appearance in the home route, we achieve the following appearance in the login route.

Dynamic Routes:

If you remember our example with React Router, I’ll explain how to achieve the same using App Router in Next.js. For those who haven’t read about React Router, let me recap the example we’re going to implement. Imagine we have an e-commerce website with thousands of different products. When a user clicks on a product, we want them to be directed to a page showing the specific details of that product. There are two ways to achieve this:

  1. Static Routes for Each Product: We could create separate components and routes for each product, as shown in the first image below.
  2. Dynamic Route Approach: Alternatively, as shown in the second image below, we can create a dynamic route where URLs like “/item-details/1”, “/item-details/2”, “/item-details/3” all render the same page component, but the content of the page changes based on the ID provided in the URL.

This approach allows us to handle potentially thousands of products without creating separate components and routes for each one.

Now, let’s see how we can implement this using App Router in Next.js.

useParams

As seen in the code above, we create the “item-details” route and below it, our [id] route, without creating thousands of components. You can also see “slug” mentioned here, which is a general term. In the page.jsx file under [id], I used the useParams hook to capture the value from the URL and displayed it on the screen. If you used [slug], you should use const {slug}. You need to match what is specified in the route to capture it correctly.

As shown in the two images below, all we do is change what comes after “/item-details”. This allows us to immediately see the output on the screen change. In other words, there’s no need to create another component or route.

How can we use this feature on our e-commerce page? After capturing the product’s ID in the URL, we can fetch details like the product’s image, name, price, etc., from the backend using the ID. This enables us to display the product details on the screen without creating individual routes and components for thousands of products. We’ve handled our task with just one route and component.

Dynamic Routing Example

Navigation:

Alright, we’ve talked about App Router quite a bit, but I still navigate to routes by manually typing URLs into the browser’s address bar. Isn’t there something like Link in React Router? Of course, there is, and it’s named the same.

As you can see in the code snippet below, we have the usage of Link on line 26. When this link is clicked, we are directed to the “/register” route. However, to navigate to other routes, it’s not necessary to click somewhere. As shown in the handleClick function, I’ve created a mock fetch function for “/api/login”. If this function returns “http status ok”, then we navigate to the home route using the “router.push(“/home”)” command. However, to use this, we need to call the useRouter function at line 6 to create an instance. Through this instance, we handle navigation.

However, useRouteris only valid for client-side components. If you want to perform navigation in server-side components, you should use the Redirectcomponent. Please visit App Router Docs for more info about Redirect.

useRouter

More To Learn:

Up to this point, I’ve covered the fundamental and frequently used aspects. In this section, I’ll briefly discuss some remarkable features that I don’t always use but find impressive. For more detailed information, you can refer to the NextJS App Router documentation.

Creating Custom Loading, Error and Not Found pages:

Creating a route can be as simple as creating a folder named “login”. Thanks to the App Router, setting up custom error and loading screens is equally straightforward. All you need to do is create files named error, not-found, and loading just like you would with a page, and then return the desired view from these files. This makes building custom pages much easier.

For example, as shown in the images below, when I encounter a “not found” error, I can specify exactly what I want to display in the not-found page. Now, whenever a "not found" error occurs, I see the content I defined.

The same approach applies to the loading and error pages. Although I'm demonstrating this with the not-found page since it's the simplest to illustrate, the principle is the same for the others.

I’m typing “/noooooooo” to URL and it returns me “not found” error

Private Folders:

Remember Route Groups? They allowed us to organize our routes and provided a more structured routing system. Typically, we would place routes like login and register under a group such as (auth), and these would become part of our routing structure.

But what if we have code that we don’t want to be part of a route? For example, suppose I have a utils.js file where I keep some helper functions. Initially, I might think to store this file in a folder named lib inside the app directory. However, this would unintentionally create a /lib route, even though there's no page file within it and I don't intend to display anything from this folder.

To handle such scenarios, NextJS provides a feature called private folders. By prefixing the folder name with an underscore (e.g., _lib instead of lib), the folder and its contents are excluded from the routing system. This means that our utils.js file can be safely stored in _lib, without turning it into a route. This keeps our helper functions private and ensures they don't interfere with our application's routing.

Parallel Routes:

Parallel routes are used when we want to render multiple routes on the same screen simultaneously. This might sound a bit complex, so let me break it down with an example.

Scenario: Building a Social Media Page

Imagine you’re developing a social media site. On your main screen, you have a scrollable feed of posts — let’s call this the home page. Additionally, you have a section for stories at the top and a sidebar on the right showing online friends.

You could code all these components directly into the home page. That way, when you navigate to the home route, it would display posts, stories, and friends all together.

The Challenge of Organizing Code

However, this approach might clutter your home page, making it harder to manage. You might want to separate the code into different folders for better organization. But you don't want to create separate routes like /stories or /friends, as these don't make sense on their own without the context of the home page.

Solution: Using Parallel Routes with Slots

Here’s where NextJS’s App Router and slots come to the rescue:

  1. Creating Slots:
  • Create a folder named @stories and place a page.jsx file inside it.
  • Similarly, create a folder named @friends and add a page.jsx file there.
  • By prefixing these folders with @, you designate them as slots. This prevents them from automatically becoming standalone routes like /stories or /friends.

2. Integration into Layout and Page Components:

  • As seen in the images below, I have two slots (@friends and @stories) alongside a layout and a page component. This page file represents my home page.
  • In my layout file, I capture these two slots and nest them under the children of the page component.
  • As shown in the final image, all components appear together under the /social route, despite being in separate files. This approach enhances code organization and simplifies development.

3. Benefits of Using Slots:

  • When each feature is separated into slots, you don’t need to wait for all slots to load to view the entire site. For instance, upon entering the home route, posts may load first, while stories are still in the loading phase, and the friends section has already loaded.
  • If an API error occurs and stories fail to load, a custom error page specific to the stories slot can be rendered. This flexibility enhances application robustness and user experience.

By leveraging NextJS’s App Router and organizing components into slots, you can effectively manage and enhance the flexibility of your application while maintaining a structured and modular codebase.

Using Intercepting Routes in Modals

Intercepting Routes is a powerful feature for creating modals, and I’ll give you an example of where and how it’s used. For detailed implementation, refer to the Intercepting Routes section on the NextJS App Router page.

Scenario: Social Media Site Modal Example

Imagine you’re on a social media site and you long-press on a photo. Without leaving the current page, the photo enlarges as a modal in the center of the screen, allowing you to view its description. All of this happens without navigating away from the current screen, achieved solely through manipulation of the App Router.

Use Case: Modals with Intercepting Routes

Intercepting Routes are typically used in scenarios like modals. Here’s how:

  • Triggering the Modal: When a user interacts with a photo (e.g., long-press), intercepting routes allow the application to display the photo details in a modal overlay.
  • Modal Behavior: The modal appears on top of the existing content, providing a focused view of the photo and its description.
  • No Page Navigation: This interaction occurs without navigating away from the current screen, enhancing user experience by maintaining context.

Conclusion:

We’ve reached the end of the article where I compared App Router and React Router, highlighting the advantages of NextJS’s App Router. I didn’t delve into all the details of React Router, nor did I cover every feature of App Router, but you can continue exploring more in the NextJS App Router documentation.

In summary NextJS’s App Router offers us several conveniences:

  • Integration into the application,
  • Ability to quickly create new routes,
  • Minimization of repetitive code with layouts,
  • Ease of managing routes with route groups,
  • Handling loading, not found, and error pages,
  • Implementing Intercepting Routes for modals,
  • Using Parallel Routes efficiently,
  • Utilizing Private Folders for code organization.

There’s a lot to learn about NextJS. I recommend trying NextJS and discovering its powerful features for yourself. Happy coding!

--

--

No responses yet