Exploring Next.js 13: App Router and Best Practices

I've been using Next.js for awhile now especially for applications where SEO, server-side rendering and performance are important. Next.js 13 introduced one of the bigger changes I've seen to the framework with the App Router and React Server Components.

Conceptually I really like the direction. Being able to make components server components by default and only push Javascript to the client where it's actually needed makes a lot of sense. At the same time I started using the App Router fairly early and ran into enough issues where I would still proceed with some caution, especially if you aren't deploying to Vercel.

Here are some of the issues I've run into so far.

Prisma and Serverless

One of the first things to be careful with is Prisma and database connections when running Next.js in a serverless environment.

Coming from a more traditional Node.js/Express application, I'm used to having a long running server with a database connection pool. Serverless changes that because the application can start multiple instances and each one can potentially create its own database connections.

This becomes really easy to overlook with the App Router because you can query the database directly from a Server Component.

export default async function Users() {  
  const users = await prisma.user.findMany();

  return (
    <div>
      {users.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
}

I actually really like being able to do this. There is much less boilerplate than creating an API endpoint just so the frontend can turn around and call it, but it also makes it easy to forget that the component is now doing backend work and has all of the same concerns as any other backend application.

If the application scales to a bunch of serverless instances you need to make sure your database can handle the connection behavior. This isn't specifically a Next.js problem but Next.js makes it extremely easy to run into.

Cloudflare Pages

Another issue I ran into was deploying Next.js to Cloudflare Pages.

Vercel obviously gets first class support for new Next.js features, but other platforms don't always support everything immediately. This was more noticeable with the App Router because there were a lot of new features being released at once.

Things like Server Components, Middleware, Route Handlers, SSR and the Edge Runtime may work differently depending on where you're hosting the application.

I've used Cloudflare quite a bit and like the platform, so my preference isn't automatically to move something to Vercel just because it's using Next.js. But it does mean I check compatibility before depending too heavily on a newer Next.js feature.

This is something I didn't think about as much with older versions of Next.js. A feature being supported by Next.js doesn't necessarily mean that feature is supported exactly the same way by the platform you deploy it to.

Server and Client Components

Probably the biggest adjustment with the App Router was getting used to where the server/client boundary should be.

By default components are Server Components, which is great until you start using a React library that expects things like state, context or browser APIs.

For example something like Material UI generally needs to run on the client:

"use client";

import { Button } from "@mui/material";

export default function MyButton() {  
  return <Button>Click Me</Button>;
}

Initially it's pretty tempting to just start adding "use client" whenever something doesn't work. That fixes the immediate problem but you can eventually end up making a huge portion of the application client-side again, which starts defeating one of the reasons for using Server Components in the first place.

I've found it better to push "use client" as far down the component tree as possible.

For example instead of:

Product Page (client)  
  Product Details
  Product Images
  Reviews
  Add To Cart

I would rather have:

Product Page (server)  
  Product Details (server)
  Product Images (server)
  Reviews (server)
  Add To Cart (client)

The Add To Cart component actually needs state and user interaction, while most of the rest of the product page doesn't.

This also seems like a good pattern for ecommerce since product pages are exactly where SEO and initial page performance matter.

Don't Move Everything to the Server

One thing I don't agree with is treating Server Components as meaning everything should now move to the server.

There is still plenty of functionality that makes more sense in the browser. Form interactions, autocomplete, modals, filters, drag/drop, realtime updates and other highly interactive functionality probably isn't improved by trying to force it into a Server Component.

I think the better way to look at the App Router is that we finally have more control over where code runs.

Before, a lot of React applications basically started with:

Everything runs in the browser  

and then we added SSR where needed.

The App Router feels closer to:

Run everything on the server  
until there is a reason for it to run in the browser  

I like this mental model much better.

Conclusion

Overall I think the App Router is the right direction for Next.js and React. Being able to fetch data directly on the server, reduce Javascript sent to the browser and decide exactly where client-side behavior starts is a big improvement.

At least when I first started using it though, I wouldn't migrate an application just because the App Router was the newest way of doing things. Prisma/serverless connections, hosting compatibility and third-party React libraries were all things I ran into that required more thought than they did with the Pages Router.

Most of those aren't reasons not to use it. They're just things I would want to figure out early instead of discovering them after the application is already in production.

Comments powered by Disqus