Blog

Early Adoption of Next.js App Router in Production: Our Thoughts

Early Adoption of Next.js App Router in Production: Our Thoughts

In May of 2023, DMC won a new project for a long-time partner of ours who specializes in separation technologies. The project consisted of a complete rewrite of their order management web portal. The old solution our partner was utilizing was written a long time ago, and none of the current engineers at their corporation confidently knew how the code functioned. This made it difficult to modernize or upgrade the site. Long story short, DMC’s task was to upgrade this early 2000s, slow web application:

Client's old website

To a faster, more modern one with a great focus on improving user experience. Some pages on the old platform took up to five minutes to load on a good day: 

Updated Client's website with DMC's new Interface

As with every major rewrite, an important early step in the process is to choose the most appropriate stack for the needs of the application. DMC's go-to stack for writing web apps consists of a React SPA front end (Single Page Application (SPA); typically scaffolded with the create-react-app tool) connected to a .NET web API backend. 

However, this project coincided with CRA starting to reach its end-of-life cycle, as well as some significant changes in the web development ecosystem. At the beginning of 2023, it seemed like the community consensus started to shift from “Client heavy” to “Server rendered; client hydrated” applications. The React.js team was also starting to adopt this trend with the announcement of React 18 that shipped with features like React server component and suspense that embrace this “server first” approach of web development.

All that to say, our partner’s web portal project was a great first opportunity for DMC to try out a new web development stack.

We had two options: either use Vite.js as an identical replacement to CRA, hook it up to a .NET backend and use our traditional development workflow, or opt in for a full stack framework like Next.js that has just become the recommended way to scaffold a React application in the “react.dev” docs. After carefully analyzing the requirements for the client’s web portal, it was obvious that the app is mostly built around heavy routing, forms, fetching, and mutating data; therefore, a client heavy solution (an SPA) seemed like overkill, and we landed on the second option. 

Coincidentally, the Next.js team has just announced the release of their brand-new app router leveraging the power of the new React 18 features mentioned above.

As you may have guessed from the title, DMC took this new app router for a ride in a production application three weeks after its release. 

Before we take a deep dive into what our team thought of this experience, here is what ended up being the entire stack for the client’s web portal rewrite. 

Client's Web Portal Rewrite

The rest of this article will be divided into the pros and cons of Next.js and whether we think it was a good idea to adopt the app router so soon after its launch. 

The Pros

1. Code Collocation

This is by far the main advantage of using a full stack framework. The fact that you have your backend and frontend code living and running in the same environment reduces a lot of the complexity that comes with the “traditional” SPA + JSON backend architecture like CORS security and making sure the server and the client are in sync at all times. 

2. Routing

Anyone who has written a React application knows that routing has always been a pinpoint of the library. Traditionally, we had to opt in for a tool like React router, which came with a lot of boilerplate and overhead. Let’s look at the React-router setup for a simple three-route app. We can clearly see how cumbersome this might become in the case of a much larger application.

Routing Library

On the other hand, routes in Next.js are built automatically at build time using the folder structure within your app router. As we can see in this example, folders are used to define routes. A route is a single path of nested folders following the file-system hierarchy from the root folder down to a final leaf folder that includes a page.js(ts) file. 

File Structure

Besides reducing overhead and boilerplate code, this opinionated file structure significantly improves the developer experience. This is because, by looking at the URL, we can directly target the part of the codebase responsible for running the page: which makes troubleshooting and debugging much easier. 

The file structure in Next.js app router does not only offer basic routing. It also provides special files like “layout.js(ts)” that provide a way for developers to share UI between multiple pages nested under the same folder path segment (and therefore under the same web route segment). On navigation, layouts preserve state and do not re-render. Here is an example from the Next.js docs that showcases this feature: 

Example from the Next.js

Similarly, Next.js makes it seamless to handle errors and loading states by leveraging the power of React’s new error boundary and suspense features. This is another Next.js feature that lifts away the complexity of manually handling loading and error states (via “useState” or any other state management mechanisms). Let’s take a look. 

The error.js file convention allows you to gracefully handle unexpected runtime errors in nested routes. Error.js automatically creates a React Error Boundary that wraps a nested child segment or page.js component. The React component exported from the error.js file is used as the fallback component. If an error is thrown within the error boundary, the error is contained, and the fallback component is rendered.

Errorsjs File Convention

Similarly, the loading.js file follows the same convention by wrapping its segments within a React suspense that offers a fallback UI to be used when those segments are waiting for the data; they need to complete their rendering.

3. Data Fetching

Now, on to my favorite part; data fetching. Getting data to power your UI is a fundamental building block of modern web development, and to be honest, is an area that React.js has historically struggled with. Traditionally, React developers had to manually roll out their data fetching solutions using the controversial “useEffect()” hook (and all the problems that came with it, but we will leave that for another discussion). Or, alternatively, opt in for a third-party library like React query, which means adding more external dependencies to your project. 

With React 18, the core team announced a new way of fetching data: React Server Components (or RSCs). RSCs are no different than your traditional React components in the sense that they are simply functions that return “JSX” elements to be converted to HTML that is then displayed on your web browser. The uniqueness of RSCs comes from the fact that you can render them on the server. This is, in my opinion, very powerful because your UI is now closer to your data, which means that it can now integrate with typical server-side operations like Input/Output. In other words, React server components give you the ability to directly access your storage resources at the component level and use the data from those resources to populate the UI. Here is an example: 

Async Function UI

Yes, it is that simple! (And yes, RSCs are allowed to be async since they are rendered on the server). 

Next.js leverages the power of both traditional (or “client” if you will) and the brand-new server components allowing developers to use the same language and the same framework to write code on both the server and the client without too much context switching.

One thing worth noting here is that each environment (server/client) has its own set of abilities and constraints. As a result, there are certain operations (mainly I/O) that are better suited for the server, whereas interactivity (and any event/state driven operations really) should be left to the client. Luckily, Next.js is very flexible when it comes to crossing that boundary between the server and the client. By default, the root component for a given tree (the one exported by the page.tsx file) is a server component. That is typically a good place to fetch data or perform any server-side logic. The data is then passed down to children of that component that can either be client or server components depending on how much interactivity is needed in that component. By simply adding the “use client” directive at the top of your component file, Next.js will know that the logic driving this component needs to be shipped to the browser. 

I think it is helpful to think of the flow of the code in your application as unidirectional. In other words, during a response, your application code flows in one direction: from the server to the client. Here is a good diagram that showcases this point:

Diagram of Code Flows from Server to Client

The advantages of the server rendered by default approach are numerous. Your web application will now have a significantly better SEO (search engine optimization) when compared to an SPA because a web crawler will now see a fully-fledged html page after sending a request to your app’s URL rather than an empty shell that only gets completed after reaching the client. This also allows for a faster initial page load as we are not waiting for the client to get all the JavaScript from the server and run it to render the UI. Speaking of which, server-side rendering significantly reduces JavaScript bundles sizes being shipped to the client as part of the UI rendering is now done on the server.

The Cons:

1. A steep learning curve: 

For experienced React developers, Next.js and React server components introduce a new mental model that requires some time to get used to. In fact, the team on the client’s project ran into several issues and bugs in the early stages of development simply because we were treating a Next.js application the same way we treat traditional single page applications; however, once the server first approach clicked and we started getting better at drawing the boundary between the server and the client, we started to see the benefits of Next js’s app router.

2. Limited resources and third-party libraries support: 

Adopting a new technology a few weeks after its release is always a challenge. Learning materials and tutorials were very hard to find and best practices and paradigms around the framework were not fully established. Furthermore, many libraries were not compatible with Next’s app router and RSCs which resulted in some difficulty setting up things like authentication and styling; however, to be fair, I have to say that this has gotten better with time and that it is much easier to start with Next.js today compared to 8 months ago.

3. Caching: 

I think this is really the only Next.js feature I dislike. By default, Next.js will try to cache as much as it can to enhance your app’s performance. This is achieved by adding multiple layers of caching mechanisms both on the server and on the client (I will not go into details here as this is quite a complex topic). The problem with this approach is that it can introduce very subtle and hard to resolve bugs to your application by causing stale data to appear throughout your UI, which leads to your server and client state going out of sync. To summarize, a new Next.js developer must take the time to read through the documentation and make sure they understand the ins and outs of the Next.js caching philosophy.

Conclusion: 

With the client’s project being in the final user testing phase, I can confidently say that Next.js’s app router was a great choice for this rewrite. It came with clear enhancements to both the end user and developer experiences. Its opinionated file structure and code collocation made it very easy for our team to onboard engineers and later pass in the code base to the client for maintenance. I am glad that we took a chance on Next.js for such a big project because it was our chance to try out a modern approach to web development and expand the DMC Application Development toolbox.

Luckily, Next.js is getting more and more adopted, and the ecosystem around it is rapidly growing, which I am confident will eliminate most of the hurdles we experienced as early adopters. I'm excited to witness the future of Next.js and the entire React.js ecosystem with this new era of server side rendered React.

Learn more about our Application Development expertise and contact us for your next project. 

Comments

There are currently no comments, be the first to post one.

Post a comment

Name (required)

Email (required)

CAPTCHA image
Enter the code shown above:

Related Blog Posts