karuna.dev

What Next.js Docs Don't Tell You

A bunch of quick facts about Next.js, App Router and RSC that you may wish you knew earlier. Updated as I discover more.

#1: Server-Side Rendering does not support authorization. Or cookies. Or custom headers.

I will likely write a separate post about this one, but here’s a brief version. Since the arrival of App Router, there are two types of components:

  1. New React Server Components (RSC), which are server only.
  2. Old Client Components, which are rendered using plain old server-side rendering (SSR) and then hydrated on the client. They now should be marked as "use client" at the top of the file.

The major thing with Client Components is that they do not have access to the request object when doing a classic SSR pass anymore. This is something you could do with getServerSideProps in the old Pages Router.

So, if you prefer using a good old isomorphic data fetching library (TanStack Query, Apollo, etc.) with useQuery or useSuspenseQuery, by default you will not be able to control the cookies or authorization headers during SSR. Depending on your APIs, it can potentially either fail completely and resort to client-side rendering or cause a hydration error because of the auth/unauth mismatch between server and browser.

My uneducated guess is that Vercel encourages using RSC for most data fetching cases, and passing the results down to Client Components. This approach does not work for me and might not work for you because caching and revalidation are different in Server Components. Fortunately, there are workarounds.

Solution 1

Because you can get cookies in RSC, you can retrieve them there, encrypt them, and pass them from RSC to Client Components. This way, SSR/browser will have them available before rendering. Encryption is needed because the cookies get exposed in the HTML response.

  1. Get the cookies/headers using cookies() or headers() in an RSC.
  2. Generate a secret key and encrypt it using the ssr-only-secrets package.
  3. Retrieve the cookies/headers in the Client Component using the helper functions from the package.
  4. Use them to initialize your Apollo/TanStack/SWR client.

This discussion and this discussion dive deeper into why we need to do this.

Solution 2

You can also accept the fact that data fetching needs to be done in RSC and keep prop drilling for everything that is not fetched dynamically. However:

#2: Server Components do not support complex search params

If you decide to put some kind of complex data using querystring or qs into the search params, be prepared that Next.js will parse it for you in their own custom way. It tries to build a dictionary for params, which is not compatible with those libraries. There is no way to access the raw query string in Server Components.

Solution 1

Use a Client Component with useSearchParams to access the native search params.

Solution 2

Encode your data using something different to prevent conflicts. For example, base64 encode a JSON.stringify call and pass is as a single search param. You can also use something like lz-string or jsoncrush for better compression.

#3: There is no way to block the navigation on the client

In a fully hydrated Next.js app, clicking a Next.js Link component will navigate using the client-side router, and there is no specific way to prevent it. Vercel’s team mentioned that they are working on a proper routing hook/event system, but for now it stays quite limited.

Solution (not really)

There is no clean solution to this. You can look for different solutions people provide in this discussion. The most common approach is to create a custom <Link /> component that handles the navigation logic. The same should be done for useRouter, you will have to either patch it or wrap most methods to prevent default behavior.

#4: history.pushState and history.replaceState do not support custom state

The documentation explicitly shows the usage of these methods to replace search params. The first argument of these methods is the state object that you can use to pass some complex data between the routes. However, it is always overwritten by Next.js.

window.history.pushState(
  { custom: "this will get overwritten anyway" }, 
  '', 
  `?${params.toString()}`
);

Solution

If you want, for example, to preserve the filters on your table when going back from the details page, you have to use a different approach like sessionStorage, localStorage or search params.