- Damian Sowa
- Read in 3 min.
Table of Contents
Over the past year, the Next.js app router has garnered attention as a powerful yet distinct approach to building React applications. My team recently started a project to rewrite an application from Vue to React. In our previous React projects, we primarily used Next.js with the pages router. However, this time, the team opted to try out the app router. Released about a year ago and recommended by the Vercel team, it offered new possibilities and challenges.
I had planned to document this journey four months ago, but the steep learning curve encouraged me to wait until I had a firmer grasp of the approach. Now, with the benefit of hindsight, I’m sharing my reflections on implementing the app router in a real-world application—how it stacks up against the pages router and whether it’s worth adopting.
App Router Basics: Server and Client Components vs. SSG, SSR, and CSR
At first glance, you might think only the terminology has changed and that everything works the same. This is not entirely true. The biggest challenges stem from not fully understanding how these components work. Additionally, I feel the Next.js documentation doesn’t provide enough information about some of the hidden mechanisms. (In fact, I contributed to the documentation to address one such undocumented behavior.)
1. Server Components
Server components are straightforward to understand:
- They are fully rendered on the server.
- They lack interactivity on the frontend.
- They can contain client components, but client components cannot contain server components.
In the app router, server components are the default. To provide limited interactivity, you can use params or search parameters. The Next.js team also recommends fetching data in server components instead of client components.
2. Client Components
This is where things get interesting. Client components are not strictly CSR (Client-Side Rendering), but they can function that way. They are technically equivalent to SSG (Static Site Generation), SSR (Server-Side Rendering), and CSR, depending on how they are used.
Client components behave similarly to components in the pages router but without functions for server control (e.g., getStaticProps
). By default, they are pre-rendered on the server. This can cause confusion: adding 'use client'
at the top of a file doesn’t stop it from running on the server.
If you’re looking for an in-depth explanation of client and server components, I recommend this excellent series of articles: A Deep Dive into Client and Server Components in Next.js 13.
State Management and Search Parameters in App Router
The app I started porting is a selling platform that heavily depends on user data. Because of the app router’s recommended usage of server components, I had to learn a new abstraction. State libraries cannot be directly accessed within server components. The Next.js team suggests using search parameters for state management.
Using Search Parameters
Search parameters work well because they are accessible on both the server and client sides.However, in practice, this approach quickly clutters the URL with multiple parameters, which developers must read, pass, and modify with every state change.
Using Zustand for Forms and Dynamic Validation
Despite the recommendation, I decided to implement a state library (Zustand) to manage data from forms, as my app required dynamic validation. All forms were therefore client components. Integrating Zustand with the app router was initially complex, but existing solutions, like those in Zustand’s documentation, were helpful. This setup only needs to be implemented once, so the initial complexity is manageable.
Alternatives: Apollo GraphQL and TanStack Query
Before the app router, Apollo GraphQL and TanStack Query were popular for managing API calls and state. However, these solutions are client-side only and don’t align well with the server-first approach of the app router.
Data Fetching in the App Router
Server Components and Data Fetching
The app router prioritizes server components for data fetching. However, this approach often requires excessive prop-passing between components because server and client components cannot share context.
The Next.js team recommends using the built-in fetch
function for data retrieval. This function is not the same as the standard fetch
in Node.js—it includes features like caching. While powerful, it’s heavily opinionated and not well-documented, so experimenting with it is essential. Server components can also be asynchronous, allowing you to use await fetch
directly for data fetching.
Server Actions
Server actions feel like an improved version of serverless functions, though they aren’t fully interchangeable. Server components have a key limitation: they lack interactivity. However, forms—reminiscent of the old PHP days—can still be utilized effectively. Server actions can process form data, validate it, and send it to the server.
Personally, I’m not a big fan of this approach because real-time validation isn’t possible. As the name suggests, server actions are executed on the server, making them more secure. They allow you to handle sensitive operations that shouldn’t run on the client side.
A great feature of server actions is that client components can directly invoke them. This eliminates the need to create serverless functions, manage API calls, or worry about network handling. Instead, you can simply connect a client component to a server action for a seamless experience.
Final Recommendation of App Router
If your project benefits from server-side functionalities, the Next.js app router can be a good choice. However, if you’re creating a simple, client-side React application and prefer a less opinionated approach, it might be worth considering pure React instead.